"""
export_io_from_CAx : Script que sirve para exraer los IOs de un proyecto de TIA Portal y generar un archivo Markdown con la información.
"""

import os
import sys
import tkinter as tk
from tkinter import filedialog
import traceback
from lxml import etree as ET
import json
from pathlib import Path
import re
import math  # Needed for ceil
import pandas as pd  # For Excel generation

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


# --- extract_aml_data function (Unchanged from v15) ---
def extract_aml_data(root):
    """(v15 logic - Unchanged) Extracts data, correcting PLC network association lookup."""
    data = {
        "plcs": {},
        "networks": {},
        "devices": {},
        "links_by_source": {},
        "links_by_target": {},
        "connections": [],
    }
    device_id_to_parent_device = {}
    all_elements = root.xpath(".//*[local-name()='InternalElement']")
    print(
        f"Pass 1: Found {len(all_elements)} InternalElement(s). Populating device dictionary..."
    )
    # (Pass 1 logic remains unchanged)
    for elem in all_elements:
        elem_id = elem.get("ID", None)
        if not elem_id:
            continue
        device_info = {
            "name": elem.get("Name", ""),
            "id": elem_id,
            "class": "",
            "type_identifier": "",
            "order_number": "",
            "type_name": "",
            "firmware_version": "",
            "position": elem.get("PositionNumber", ""),
            "attributes": {},
            "interfaces": [],
            "network_nodes": [],
            "io_addresses": [],
            "children_ids": [
                c.get("ID")
                for c in elem.xpath("./*[local-name()='InternalElement']")
                if c.get("ID")
            ],
            "parent_id": (
                elem.xpath("parent::*[local-name()='InternalElement']/@ID")[0]
                if elem.xpath("parent::*[local-name()='InternalElement']")
                else None
            ),
        }
        class_tag = elem.xpath("./*[local-name()='SystemUnitClass']")
        device_info["class"] = (
            class_tag[0].get("Path", elem.get("RefBaseSystemUnitPath", ""))
            if class_tag
            else elem.get("RefBaseSystemUnitPath", "")
        )
        attributes = elem.xpath("./*[local-name()='Attribute']")
        for attr in attributes:
            attr_name = attr.get("Name", "")
            value_elem = attr.xpath("./*[local-name()='Value']/text()")
            attr_value = value_elem[0] if value_elem else ""
            device_info["attributes"][attr_name] = attr_value
            if attr_name == "TypeIdentifier":
                device_info["type_identifier"] = attr_value
            if "OrderNumber:" in attr_value:
                device_info["order_number"] = attr_value.split("OrderNumber:")[
                    -1
                ].strip()
            elif attr_name == "TypeName":
                device_info["type_name"] = attr_value
            elif attr_name == "FirmwareVersion":
                device_info["firmware_version"] = attr_value
            elif attr_name == "Address":
                address_parts = attr.xpath("./*[local-name()='Attribute']")
                for part in address_parts:
                    addr_details = {
                        "area": part.get("Name", "?"),
                        "start": "",
                        "length": "",
                        "type": "",
                    }
                    start_val = part.xpath(
                        "./*[local-name()='Attribute'][@Name='StartAddress']/*[local-name()='Value']/text()"
                    )
                    len_val = part.xpath(
                        "./*[local-name()='Attribute'][@Name='Length']/*[local-name()='Value']/text()"
                    )
                    type_val = part.xpath(
                        "./*[local-name()='Attribute'][@Name='IoType']/*[local-name()='Value']/text()"
                    )
                    if start_val:
                        addr_details["start"] = start_val[0]
                    if len_val:
                        addr_details["length"] = len_val[0]
                    if type_val:
                        addr_details["type"] = type_val[0]
                    if addr_details["start"] != "":
                        device_info["io_addresses"].append(addr_details)
        interfaces = elem.xpath("./*[local-name()='ExternalInterface']")
        for interface in interfaces:
            device_info["interfaces"].append(
                {
                    "name": interface.get("Name", ""),
                    "id": interface.get("ID", ""),
                    "ref_base_class": interface.get("RefBaseClassPath", ""),
                }
            )
        network_node_elements = elem.xpath(
            "./*[local-name()='InternalElement'][*[local-name()='SupportedRoleClass'][contains(@RefRoleClassPath, 'Node')]]"
        )
        if not network_node_elements and elem.xpath(
            "./*[local-name()='SupportedRoleClass'][contains(@RefRoleClassPath, 'Node')]"
        ):
            network_node_elements = [elem]
        for node_elem in network_node_elements:
            node_id = node_elem.get("ID")
            if not node_id:
                continue
            node_info = {
                "id": node_id,
                "name": node_elem.get("Name", device_info["name"]),
                "type": "",
                "address": "",
            }
            type_attr = node_elem.xpath(
                "./*[local-name()='Attribute'][@Name='Type']/*[local-name()='Value']/text()"
            )
            addr_attr = node_elem.xpath(
                "./*[local-name()='Attribute'][@Name='NetworkAddress']/*[local-name()='Value']/text()"
            )
            if type_attr:
                node_info["type"] = type_attr[0]
            if addr_attr:
                node_info["address"] = addr_attr[0]
            if not node_info["address"]:
                parent_addr_attr = elem.xpath(
                    "./*[local-name()='Attribute'][@Name='NetworkAddress']/*[local-name()='Value']/text()"
                )
                if parent_addr_attr:
                    node_info["address"] = parent_addr_attr[0]
            if not node_info["type"]:
                parent_type_attr = elem.xpath(
                    "./*[local-name()='Attribute'][@Name='Type']/*[local-name()='Value']/text()"
                )
                if parent_type_attr:
                    node_info["type"] = parent_type_attr[0]
            if node_info["address"]:
                len_attr = node_elem.xpath(
                    "./*[local-name()='Attribute'][@Name='Length']/*[local-name()='Value']/text()"
                )
                node_info["length"] = len_attr[0] if len_attr else ""
                device_info["network_nodes"].append(node_info)
                device_id_to_parent_device[node_id] = elem_id
        data["devices"][elem_id] = device_info

    print("Pass 2: Identifying PLCs and Networks (Refined v2)...")
    plc_ids_found = set()
    elem_map = {elem.get("ID"): elem for elem in all_elements if elem.get("ID")}
    for dev_id, device in data["devices"].items():
        is_plc = False
        plc_order_prefixes = [
            "6ES7 516-3FP03",
            "6ES7 151",
            "6ES7 31",
            "6ES7 41",
            "6ES7 51",
        ]
        if any(
            device.get("order_number", "").startswith(prefix)
            for prefix in plc_order_prefixes
        ):
            is_plc = True
        elif (
            "CPU" in device.get("type_name", "").upper()
            or "PLC" in device.get("type_name", "").upper()
        ):
            is_plc = True
        if is_plc:
            parent_id = device.get("parent_id")
            is_child_of_plc = False
            current_parent = parent_id
            while current_parent:
                if current_parent in plc_ids_found:
                    is_child_of_plc = True
                    break
                current_parent = (
                    data["devices"].get(current_parent, {}).get("parent_id")
                )
            if not is_child_of_plc:
                if dev_id not in plc_ids_found:
                    print(
                        f"  Identified PLC: {device['name']} ({dev_id}) - Type: {device.get('type_name', '')} OrderNo: {device.get('order_number', '')}"
                    )
                    device["connected_networks"] = {}
                    data["plcs"][dev_id] = device
                    plc_ids_found.add(dev_id)
        is_network = False
        net_type = "Unknown"
        elem = elem_map.get(dev_id)
        if elem is not None:
            role_classes = elem.xpath(
                "./*[local-name()='SupportedRoleClass']/@RefRoleClassPath"
            )
            is_subnet_by_role = any("SUBNET" in rc.upper() for rc in role_classes)
            if is_subnet_by_role:
                is_network = True
                # Check role classes first
                for rc in role_classes:
                    rc_upper = rc.upper()
                    if "PROFINET" in rc_upper or "ETHERNET" in rc_upper:
                        net_type = "Ethernet/Profinet"
                        break
                    elif "PROFIBUS" in rc_upper:
                        net_type = "Profibus"
                        break

                # If still unknown, check the Type attribute (crucial for PROFINET)
                if net_type == "Unknown":
                    type_attr = device.get("attributes", {}).get("Type", "")
                    if type_attr.upper() == "ETHERNET":
                        net_type = "Ethernet/Profinet"
                    elif type_attr.upper() == "PROFIBUS":
                        net_type = "Profibus"

                # Finally, check device name as fallback
                if net_type == "Unknown":
                    if "PROFIBUS" in device["name"].upper():
                        net_type = "Profibus"
                    elif (
                        "ETHERNET" in device["name"].upper()
                        or "PROFINET" in device["name"].upper()
                        or "PN/IE"
                        in device["name"].upper()  # Add common PROFINET naming pattern
                    ):
                        net_type = "Ethernet/Profinet"
        if is_network:
            if dev_id not in data["networks"]:
                print(
                    f"  Identified Network: {device['name']} ({dev_id}) Type: {net_type}"
                )
                data["networks"][dev_id] = {
                    "name": device["name"],
                    "type": net_type,
                    "devices_on_net": {},
                }

    print("Pass 3: Processing InternalLinks (Robust Network Mapping & IO)...")
    internal_links = root.xpath(".//*[local-name()='InternalLink']")
    print(f"Found {len(internal_links)} InternalLink(s).")
    conn_id_counter = 0
    for link in internal_links:
        conn_id_counter += 1
        link_name = link.get("Name", f"Link_{conn_id_counter}")
        side_a_ref = link.get("RefPartnerSideA", "")
        side_b_ref = link.get("RefPartnerSideB", "")
        side_a_match = re.match(r"([^:]+):?(.*)", side_a_ref)
        side_b_match = re.match(r"([^:]+):?(.*)", side_b_ref)
        side_a_id = side_a_match.group(1) if side_a_match else ""
        side_a_suffix = (
            side_a_match.group(2)
            if side_a_match and side_a_match.group(2)
            else side_a_id
        )
        side_b_id = side_b_match.group(1) if side_b_match else ""
        side_b_suffix = (
            side_b_match.group(2)
            if side_b_match and side_b_match.group(2)
            else side_b_id
        )
        network_id, device_node_id = None, None
        side_a_is_network = side_a_id in data["networks"]
        side_b_is_network = side_b_id in data["networks"]
        if side_a_is_network and not side_b_is_network:
            network_id, device_node_id = side_a_id, side_b_id
        elif side_b_is_network and not side_a_is_network:
            network_id, device_node_id = side_b_id, side_a_id
        elif side_a_is_network and side_b_is_network:
            continue
        elif not side_a_is_network and not side_b_is_network:
            pass
        if network_id and device_node_id:
            linked_device_id = device_id_to_parent_device.get(device_node_id)
            if not linked_device_id and device_node_id in data["devices"]:
                linked_device_id = device_node_id
            if linked_device_id and linked_device_id in data["devices"]:
                device_info = data["devices"].get(linked_device_id)
                if not device_info:
                    continue
                address = ""
                node_info_for_addr = data["devices"].get(device_node_id, {})
                for node in node_info_for_addr.get("network_nodes", []):
                    if node.get("id") == device_node_id:
                        address = node.get("address", "")
                        break
                if not address:
                    address = node_info_for_addr.get("attributes", {}).get(
                        "NetworkAddress", ""
                    )
                if not address:
                    address = device_info.get("attributes", {}).get(
                        "NetworkAddress", ""
                    )
                node_name_for_log = node_info_for_addr.get("name", device_node_id)
                print(
                    f"  Mapping Device/Node '{node_name_for_log}' (NodeID:{device_node_id}, Addr:{address}) to Network '{data['networks'][network_id]['name']}'"
                )
                data["networks"][network_id]["devices_on_net"][device_node_id] = address
                potential_plc_id = None
                interface_id = None
                interface_info = None
                node_check_info = data["devices"].get(device_node_id)
                if node_check_info:
                    if device_node_id in data["plcs"]:
                        potential_plc_id = device_node_id
                    else:
                        interface_id = node_check_info.get("parent_id")
                        if interface_id and interface_id in data["devices"]:
                            interface_info = data["devices"].get(interface_id)
                            if interface_info:
                                if interface_id in data["plcs"]:
                                    potential_plc_id = interface_id
                                elif (
                                    interface_info.get("parent_id")
                                    and interface_info["parent_id"] in data["plcs"]
                                ):
                                    potential_plc_id = interface_info["parent_id"]
                # Enhanced PLC search: look for PLCs in the entire hierarchy
                if not potential_plc_id:
                    # Search for PLCs in the hierarchy starting from linked_device_id
                    current_search_id = linked_device_id
                    search_depth = 0
                    max_search_depth = 10

                    while current_search_id and search_depth < max_search_depth:
                        # Check current device and all its children for PLCs
                        device_to_check = data["devices"].get(current_search_id)
                        if device_to_check:
                            # Check if current device has PLCs as children
                            for child_id in device_to_check.get("children_ids", []):
                                if child_id in data["plcs"]:
                                    potential_plc_id = child_id
                                    print(
                                        f"    --> Found PLC in children: {data['plcs'][child_id].get('name', 'Unknown PLC')} (ID: {child_id})"
                                    )
                                    break

                            if potential_plc_id:
                                break

                            # Move up to parent
                            current_search_id = device_to_check.get("parent_id")
                            search_depth += 1
                        else:
                            break

                if potential_plc_id:
                    plc_object = data["plcs"][potential_plc_id]
                    if "connected_networks" not in plc_object:
                        plc_object["connected_networks"] = {}
                    if network_id not in plc_object.get("connected_networks", {}):
                        print(
                            f"    --> Associating Network '{data['networks'][network_id]['name']}' with PLC '{plc_object.get('name', 'Unknown PLC')}' (via Node '{node_name_for_log}' Addr: {address})"
                        )
                        data["plcs"][potential_plc_id]["connected_networks"][
                            network_id
                        ] = address
                    elif not plc_object["connected_networks"][network_id] and address:
                        print(
                            f"    --> Updating address for Network '{data['networks'][network_id]['name']}' on PLC '{plc_object.get('name', 'Unknown PLC')}' to: {address}"
                        )
                        data["plcs"][potential_plc_id]["connected_networks"][
                            network_id
                        ] = address
            else:
                print(
                    f"  Warning: Could not map linked device ID {linked_device_id} (from NodeID {device_node_id}) to any known device."
                )
            continue
        if not network_id:  # Generic links
            source_id, source_suffix, target_id, target_suffix = (
                side_a_id,
                side_a_suffix,
                side_b_id,
                side_b_suffix,
            )
            if ("Channel" in side_b_suffix or "Parameter" in side_b_suffix) and (
                "Channel" not in side_a_suffix and "Parameter" not in side_a_suffix
            ):
                source_id, source_suffix, target_id, target_suffix = (
                    side_b_id,
                    side_b_suffix,
                    side_a_id,
                    side_a_suffix,
                )
            if source_id and target_id:
                if source_id not in data["links_by_source"]:
                    data["links_by_source"][source_id] = []
                if target_id not in data["links_by_target"]:
                    data["links_by_target"][target_id] = []
                link_detail = {
                    "name": link_name,
                    "source_id": source_id,
                    "source_suffix": source_suffix,
                    "target_id": target_id,
                    "target_suffix": target_suffix,
                    "source_device_name": data["devices"]
                    .get(source_id, {})
                    .get("name", source_id),
                    "target_device_name": data["devices"]
                    .get(target_id, {})
                    .get("name", target_id),
                }
                data["links_by_source"][source_id].append(link_detail)
                data["links_by_target"][target_id].append(link_detail)
                data["connections"].append(link_detail)
    print("Data extraction and structuring complete.")
    return data


# --- Helper Function for Recursive IO Search (Unchanged from v20) ---
def find_io_recursively(device_id, project_data, module_context):
    """
    Recursively finds all IO addresses under a given device ID, using module_context
    for details of the main hardware module.
    module_context = {"id": ..., "name": ..., "order_number": ..., "type_name": ...}
    """
    io_list = []
    device_info = project_data.get("devices", {}).get(device_id)
    if not device_info:
        return io_list

    if device_info.get("io_addresses"):
        # Slot position is from the current device_info (which holds the IO)
        # It's often found in attributes.PositionNumber for sub-elements.
        slot_pos = device_info.get("attributes", {}).get(
            "PositionNumber", device_info.get("position", "")
        )

        for addr in device_info["io_addresses"]:
            io_list.append(
                {
                    "module_id": module_context["id"],
                    "module_name": module_context["name"],
                    "module_pos": slot_pos,  # Slot of the IO sub-element
                    "module_order_number": module_context["order_number"],
                    "module_type_name": module_context["type_name"],
                    **addr,
                }
            )

    children_ids = device_info.get("children_ids", [])
    for child_id in children_ids:
        if child_id != device_id:  # Basic loop prevention
            # The module_context remains the same as we recurse *within* a main module's structure
            io_list.extend(find_io_recursively(child_id, project_data, module_context))
    return io_list


# --- generate_io_summary_file function (Updated) ---
def generate_io_summary_file(
    all_plc_io_for_table, md_file_path, plc_name, project_data, output_root_path
):
    """
    Generates a Hardware.md file with the IO summary table.
    If there's only one PLC, creates it in the root directory, otherwise creates PLC-specific named files.
    """

    # Determine if this is the only PLC in the project
    plcs_count = len(project_data.get("plcs", {}))
    is_single_plc = plcs_count == 1

    if is_single_plc:
        # For single PLC: create Hardware.md in the root directory
        hardware_file_path = os.path.join(output_root_path, "Hardware.md")
        file_title = f"# IO Summary Table for PLC: {plc_name}"
    else:
        # For multiple PLCs: create [PLC_Name]_Hardware.md in PLC's directory
        hardware_file_path = os.path.join(
            os.path.dirname(md_file_path), f"{sanitize_filename(plc_name)}_Hardware.md"
        )
        file_title = f"# IO Summary Table for PLC: {plc_name}"

    markdown_lines = [file_title, ""]

    if all_plc_io_for_table:
        # Define table headers
        headers = [
            "Network",
            "Type",
            "Address",
            "Device Name",
            "Sub-Device",
            "OrderNo",
            "Type",
            "IO Type",
            "IO Address",
            "Number of Bits",
        ]
        markdown_lines.append("| " + " | ".join(headers) + " |")
        markdown_lines.append("|-" + "-|-".join(["---"] * len(headers)) + "-|")

        # Sort the collected data
        sorted_table_data = sorted(all_plc_io_for_table, key=lambda x: x["SortKey"])

        # Add rows to the table
        for row_data in sorted_table_data:
            row = [
                row_data.get("Network", ""),
                row_data.get("Network Type", ""),
                row_data.get("Device Address", ""),
                row_data.get("Device Name", ""),
                row_data.get("Sub-Device", ""),
                row_data.get("Sub-Device OrderNo", ""),
                row_data.get("Sub-Device Type", ""),
                row_data.get("IO Type", ""),
                f"`{row_data.get('IO Address', '')}`",  # Format IO Address as code
                row_data.get("Number of Bits", ""),
            ]
            # Escape pipe characters within cell content if necessary
            row = [str(cell).replace("|", "\\|") for cell in row]
            markdown_lines.append("| " + " | ".join(row) + " |")
    else:
        markdown_lines.append("*No IO data found for this PLC.*")

    try:
        with open(hardware_file_path, "w", encoding="utf-8") as f:
            f.write("\n".join(markdown_lines))
        print(f"IO summary table written to: {hardware_file_path}")
    except Exception as e:
        print(f"ERROR writing Hardware.md file {hardware_file_path}: {e}")
        traceback.print_exc()

    return hardware_file_path


# --- generate_io_excel_report function ---
def generate_io_excel_report(
    project_data, excel_file_path, target_plc_id, output_root_path
):
    """
    Genera un archivo Excel con información detallada de IOs por nodos del PLC.
    """

    plc_info = project_data.get("plcs", {}).get(target_plc_id)
    if not plc_info:
        print(f"PLC ID '{target_plc_id}' not found in project data.")
        return

    plc_name = plc_info.get("name", target_plc_id)
    print(f"Generating Excel IO report for PLC: {plc_name}")

    # Lista para almacenar todas las filas del Excel
    excel_rows = []

    # v32.5: First, process PLC's local modules (modules in the same rack/structure as the PLC)
    plc_parent_id = plc_info.get("parent_id")
    if plc_parent_id:
        # Find sibling modules in the same parent structure (rack)
        for dev_id, dev_info in project_data.get("devices", {}).items():
            if dev_info.get("parent_id") == plc_parent_id and dev_id != target_plc_id:
                # This is a sibling module of the PLC
                module_context = {
                    "id": dev_id,
                    "name": dev_info.get("name", dev_id),
                    "order_number": dev_info.get("order_number", ""),
                    "type_name": dev_info.get("type_name", ""),
                }
                module_ios = find_io_recursively(dev_id, project_data, module_context)
                if module_ios:
                    # Agrupar IOs por módulo para este módulo local
                    ios_by_module = {}
                    for addr_info in module_ios:
                        module_id = addr_info.get("module_id")
                        if module_id not in ios_by_module:
                            ios_by_module[module_id] = {
                                "module_info": {
                                    "name": addr_info.get("module_name", "?"),
                                    "type": addr_info.get("module_type_name", ""),
                                    "order": addr_info.get("module_order_number", ""),
                                    "position": addr_info.get("module_pos", ""),
                                },
                                "inputs": [],
                                "outputs": [],
                            }

                        # Clasificar IO como input u output
                        io_type = addr_info.get("type", "").lower()
                        if io_type == "input":
                            ios_by_module[module_id]["inputs"].append(addr_info)
                        elif io_type == "output":
                            ios_by_module[module_id]["outputs"].append(addr_info)

                    # Crear una fila por cada módulo local con IOs
                    for module_id, module_data in ios_by_module.items():
                        module_info = module_data["module_info"]

                        # Calcular direcciones de entrada - Start + Word Count
                        input_start_addr = ""
                        input_word_count = 0
                        total_input_bits = 0

                        for addr_info in module_data["inputs"]:
                            start_str = addr_info.get("start", "?")
                            length_str = addr_info.get("length", "?")
                            try:
                                start_byte = int(start_str)
                                length_bits = int(length_str)
                                length_bytes = math.ceil(length_bits / 8.0)
                                if length_bits > 0 and length_bytes == 0:
                                    length_bytes = 1

                                # Para múltiples rangos, tomar el primer inicio y sumar words
                                if not input_start_addr:
                                    input_start_addr = start_byte
                                else:
                                    input_start_addr = min(input_start_addr, start_byte)

                                # Convertir bytes a words (asumiendo words de 2 bytes)
                                word_count = math.ceil(length_bytes / 2.0)
                                input_word_count += word_count
                                total_input_bits += length_bits
                            except:
                                # En caso de error, mantener N/A
                                pass

                        # Calcular direcciones de salida - Start + Word Count
                        output_start_addr = ""
                        output_word_count = 0
                        total_output_bits = 0

                        for addr_info in module_data["outputs"]:
                            start_str = addr_info.get("start", "?")
                            length_str = addr_info.get("length", "?")
                            try:
                                start_byte = int(start_str)
                                length_bits = int(length_str)
                                length_bytes = math.ceil(length_bits / 8.0)
                                if length_bits > 0 and length_bytes == 0:
                                    length_bytes = 1

                                # Para múltiples rangos, tomar el primer inicio y sumar words
                                if not output_start_addr:
                                    output_start_addr = start_byte
                                else:
                                    output_start_addr = min(
                                        output_start_addr, start_byte
                                    )

                                # Convertir bytes a words (asumiendo words de 2 bytes)
                                word_count = math.ceil(length_bytes / 2.0)
                                output_word_count += word_count
                                total_output_bits += length_bits
                            except:
                                # En caso de error, mantener N/A
                                pass

                        excel_rows.append(
                            {
                                "PLC Name": plc_name,
                                "Network Path": f"Local I/O -> {module_info['name']}",
                                "Network Type": "Local I/O",
                                "Device Address": "Local",
                                "Device Name": f"PLC {plc_name}",
                                "Device Type": module_info["type"],
                                "Order Number": module_info["order"],
                                "Firmware Version": "",
                                "Position": module_info["position"],
                                "IO Input Start Address": input_start_addr,
                                "IO Input Word Count": (
                                    input_word_count if input_word_count > 0 else ""
                                ),
                                "IO Output Start Address": output_start_addr,
                                "IO Output Word Count": (
                                    output_word_count if output_word_count > 0 else ""
                                ),
                                "Total Input Bits": total_input_bits,
                                "Total Output Bits": total_output_bits,
                                "Module Name": module_info["name"],
                                "Module Type": module_info["type"],
                                "Module Order Number": module_info["order"],
                            }
                        )

    # Procesar las redes conectadas al PLC
    plc_networks = plc_info.get("connected_networks", {})

    if not plc_networks and not excel_rows:
        # Si no hay redes, crear una fila básica del PLC
        excel_rows.append(
            {
                "PLC Name": plc_name,
                "Network Path": "No networks connected",
                "Network Type": "",
                "Device Address": "",
                "Device Name": plc_name,
                "Device Type": plc_info.get("type_name", ""),
                "Order Number": plc_info.get("order_number", ""),
                "Firmware Version": plc_info.get("firmware_version", ""),
                "Position": plc_info.get("position", ""),
                "IO Input Start Address": "",
                "IO Input Word Count": "",
                "IO Output Start Address": "",
                "IO Output Word Count": "",
                "Total Input Bits": 0,
                "Total Output Bits": 0,
                "Module Name": "",
                "Module Type": "",
                "Module Order Number": "",
            }
        )
    else:
        # Procesar cada red conectada
        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

            network_name = net_info.get("name", net_id)
            network_type = net_info.get("type", "Unknown")
            devices_on_net = net_info.get("devices_on_net", {})

            # Identificar nodos que pertenecen al PLC para excluirlos de la lista de dispositivos
            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)

            # Filtrar dispositivos que no son interfaces del PLC
            other_devices = [
                (node_id, node_addr)
                for node_id, node_addr in devices_on_net.items()
                if node_id not in plc_interface_and_node_ids
            ]

            if not other_devices:
                # Si no hay otros dispositivos, crear fila solo para el PLC en esta red
                excel_rows.append(
                    {
                        "PLC Name": plc_name,
                        "Network Path": f"{network_name} -> {plc_name}",
                        "Network Type": network_type,
                        "Device Address": plc_addr_on_net,
                        "Device Name": plc_name,
                        "Device Type": plc_info.get("type_name", ""),
                        "Order Number": plc_info.get("order_number", ""),
                        "Firmware Version": plc_info.get("firmware_version", ""),
                        "Position": plc_info.get("position", ""),
                        "IO Input Start Address": "",
                        "IO Input Word Count": "",
                        "IO Output Start Address": "",
                        "IO Output Word Count": "",
                        "Total Input Bits": 0,
                        "Total Output Bits": 0,
                        "Module Name": "PLC Main Unit",
                        "Module Type": plc_info.get("type_name", ""),
                        "Module Order Number": plc_info.get("order_number", ""),
                    }
                )
            else:
                # Procesar cada dispositivo en la red
                for node_id, node_addr in other_devices:
                    node_info = project_data.get("devices", {}).get(node_id)
                    if not node_info:
                        continue

                    # Determinar la estructura jerárquica 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)

                    # Determinar qué información mostrar
                    display_info = (
                        actual_device_info
                        if actual_device_info
                        else (interface_info if interface_info else node_info)
                    )
                    display_id = (
                        actual_device_id
                        if actual_device_info
                        else (interface_id if interface_info else node_id)
                    )

                    device_name = display_info.get("name", display_id)
                    device_type = display_info.get("type_name", "")
                    device_order = display_info.get("order_number", "")
                    device_position = display_info.get("position", "")
                    firmware_version = display_info.get("firmware_version", "")

                    # Construir el path de red
                    network_path = f"{network_name} ({network_type}) -> {device_name} @ {node_addr}"

                    # Buscar IOs recursivamente
                    io_search_root_id = display_id
                    io_search_root_info = project_data.get("devices", {}).get(
                        io_search_root_id
                    )

                    aggregated_io_addresses = []

                    # Buscar IOs en la estructura padre si existe
                    parent_structure_id = (
                        io_search_root_info.get("parent_id")
                        if io_search_root_info
                        else None
                    )

                    if parent_structure_id:
                        # Buscar IOs en dispositivos hermanos bajo la misma estructura padre
                        for dev_scan_id, dev_scan_info in project_data.get(
                            "devices", {}
                        ).items():
                            if dev_scan_info.get("parent_id") == parent_structure_id:
                                module_context = {
                                    "id": dev_scan_id,
                                    "name": dev_scan_info.get("name", dev_scan_id),
                                    "order_number": dev_scan_info.get(
                                        "order_number", ""
                                    ),
                                    "type_name": dev_scan_info.get("type_name", ""),
                                }
                                io_from_sibling = find_io_recursively(
                                    dev_scan_id, project_data, module_context
                                )
                                aggregated_io_addresses.extend(io_from_sibling)
                    elif io_search_root_id:
                        # Buscar IOs directamente en el dispositivo
                        module_context = {
                            "id": io_search_root_id,
                            "name": io_search_root_info.get("name", io_search_root_id),
                            "order_number": io_search_root_info.get("order_number", ""),
                            "type_name": io_search_root_info.get("type_name", ""),
                        }
                        aggregated_io_addresses = find_io_recursively(
                            io_search_root_id, project_data, module_context
                        )

                    # Procesar IOs por módulo
                    if aggregated_io_addresses:
                        # Agrupar IOs por módulo
                        ios_by_module = {}
                        for addr_info in aggregated_io_addresses:
                            module_id = addr_info.get("module_id")
                            if module_id not in ios_by_module:
                                ios_by_module[module_id] = {
                                    "module_info": {
                                        "name": addr_info.get("module_name", "?"),
                                        "type": addr_info.get("module_type_name", ""),
                                        "order": addr_info.get(
                                            "module_order_number", ""
                                        ),
                                        "position": addr_info.get("module_pos", ""),
                                    },
                                    "inputs": [],
                                    "outputs": [],
                                }

                            # Clasificar IO como input u output
                            io_type = addr_info.get("type", "").lower()
                            if io_type == "input":
                                ios_by_module[module_id]["inputs"].append(addr_info)
                            elif io_type == "output":
                                ios_by_module[module_id]["outputs"].append(addr_info)

                        # Crear una fila por cada módulo con IOs
                        for module_id, module_data in ios_by_module.items():
                            module_info = module_data["module_info"]

                            # Calcular direcciones de entrada - Start + Word Count
                            input_start_addr = ""
                            input_word_count = 0
                            total_input_bits = 0

                            for addr_info in module_data["inputs"]:
                                start_str = addr_info.get("start", "?")
                                length_str = addr_info.get("length", "?")
                                try:
                                    start_byte = int(start_str)
                                    length_bits = int(length_str)
                                    length_bytes = math.ceil(length_bits / 8.0)
                                    if length_bits > 0 and length_bytes == 0:
                                        length_bytes = 1

                                    # Para múltiples rangos, tomar el primer inicio y sumar words
                                    if not input_start_addr:
                                        input_start_addr = start_byte
                                    else:
                                        input_start_addr = min(
                                            input_start_addr, start_byte
                                        )

                                    # Convertir bytes a words (asumiendo words de 2 bytes)
                                    word_count = math.ceil(length_bytes / 2.0)
                                    input_word_count += word_count
                                    total_input_bits += length_bits
                                except:
                                    # En caso de error, mantener N/A
                                    pass

                            # Calcular direcciones de salida - Start + Word Count
                            output_start_addr = ""
                            output_word_count = 0
                            total_output_bits = 0

                            for addr_info in module_data["outputs"]:
                                start_str = addr_info.get("start", "?")
                                length_str = addr_info.get("length", "?")
                                try:
                                    start_byte = int(start_str)
                                    length_bits = int(length_str)
                                    length_bytes = math.ceil(length_bits / 8.0)
                                    if length_bits > 0 and length_bytes == 0:
                                        length_bytes = 1

                                    # Para múltiples rangos, tomar el primer inicio y sumar words
                                    if not output_start_addr:
                                        output_start_addr = start_byte
                                    else:
                                        output_start_addr = min(
                                            output_start_addr, start_byte
                                        )

                                    # Convertir bytes a words (asumiendo words de 2 bytes)
                                    word_count = math.ceil(length_bytes / 2.0)
                                    output_word_count += word_count
                                    total_output_bits += length_bits
                                except:
                                    # En caso de error, mantener N/A
                                    pass

                            excel_rows.append(
                                {
                                    "PLC Name": plc_name,
                                    "Network Path": network_path,
                                    "Network Type": network_type,
                                    "Device Address": node_addr,
                                    "Device Name": device_name,
                                    "Device Type": device_type,
                                    "Order Number": device_order,
                                    "Firmware Version": firmware_version,
                                    "Position": device_position,
                                    "IO Input Start Address": input_start_addr,
                                    "IO Input Word Count": (
                                        input_word_count if input_word_count > 0 else ""
                                    ),
                                    "IO Output Start Address": output_start_addr,
                                    "IO Output Word Count": (
                                        output_word_count
                                        if output_word_count > 0
                                        else ""
                                    ),
                                    "Total Input Bits": total_input_bits,
                                    "Total Output Bits": total_output_bits,
                                    "Module Name": module_info["name"],
                                    "Module Type": module_info["type"],
                                    "Module Order Number": module_info["order"],
                                }
                            )
                    else:
                        # Dispositivo sin IOs
                        excel_rows.append(
                            {
                                "PLC Name": plc_name,
                                "Network Path": network_path,
                                "Network Type": network_type,
                                "Device Address": node_addr,
                                "Device Name": device_name,
                                "Device Type": device_type,
                                "Order Number": device_order,
                                "Firmware Version": firmware_version,
                                "Position": device_position,
                                "IO Input Start Address": "",
                                "IO Input Word Count": "",
                                "IO Output Start Address": "",
                                "IO Output Word Count": "",
                                "Total Input Bits": 0,
                                "Total Output Bits": 0,
                                "Module Name": "",
                                "Module Type": "",
                                "Module Order Number": "",
                            }
                        )

    # Crear DataFrame y guardar Excel
    if excel_rows:
        df = pd.DataFrame(excel_rows)

        # Agregar columna de ID único para compatibilidad con x7_update_CAx
        df["Unique_ID"] = df["PLC Name"] + "+" + df["Device Name"]

        # Reordenar columnas para mejor legibilidad
        column_order = [
            "PLC Name",
            "Network Path",
            "Network Type",
            "Device Address",
            "Device Name",
            "Device Type",
            "Order Number",
            "Firmware Version",
            "Position",
            "Module Name",
            "Module Type",
            "Module Order Number",
            "IO Input Start Address",
            "IO Input Word Count",
            "IO Output Start Address",
            "IO Output Word Count",
            "Total Input Bits",
            "Total Output Bits",
            "Unique_ID",  # Agregar al final para compatibilidad
        ]
        df = df.reindex(columns=column_order)

        try:
            # Convertir columnas numéricas específicas a int para evitar decimales
            numeric_columns = [
                "IO Input Start Address",
                "IO Input Word Count",
                "IO Output Start Address",
                "IO Output Word Count",
                "Total Input Bits",
                "Total Output Bits",
            ]

            for col in numeric_columns:
                if col in df.columns:
                    # Convertir a int manteniendo N/A como string
                    df[col] = df[col].apply(
                        lambda x: (
                            int(x) if isinstance(x, (int, float)) and pd.notna(x) else x
                        )
                    )

            # Guardar como Excel con formato
            with pd.ExcelWriter(excel_file_path, engine="openpyxl") as writer:
                df.to_excel(writer, sheet_name="IO Report", index=False)

                # Ajustar ancho de columnas
                worksheet = writer.sheets["IO Report"]
                for column in worksheet.columns:
                    max_length = 0
                    column_letter = column[0].column_letter
                    for cell in column:
                        try:
                            if len(str(cell.value)) > max_length:
                                max_length = len(str(cell.value))
                        except:
                            pass
                    adjusted_width = min(max_length + 2, 50)  # Máximo 50 caracteres
                    worksheet.column_dimensions[column_letter].width = adjusted_width

            print(f"Excel IO report saved to: {excel_file_path}")
            print(f"Total rows in report: {len(excel_rows)}")

        except Exception as e:
            print(f"ERROR saving Excel file {excel_file_path}: {e}")
            traceback.print_exc()
    else:
        print("No data to write to Excel file.")


# --- generate_markdown_tree function ---
def generate_markdown_tree(project_data, md_file_path, target_plc_id, output_root_path):
    """(Modified) Generates hierarchical Markdown for a specific PLC."""

    plc_info = project_data.get("plcs", {}).get(target_plc_id)
    plc_name_for_title = "Unknown PLC"
    if plc_info:
        plc_name_for_title = plc_info.get("name", target_plc_id)

    # v31: Initialize list to store all IO data for the summary table for this PLC
    all_plc_io_for_table = []

    markdown_lines = [f"# Hardware & IO Summary for PLC: {plc_name_for_title}", ""]

    if not plc_info:
        markdown_lines.append(
            f"*Details for PLC ID '{target_plc_id}' not found in the project data.*"
        )
        try:
            with open(md_file_path, "w", encoding="utf-8") as f:
                f.write("\n".join(markdown_lines))
            print(f"Markdown summary (PLC not found) written to: {md_file_path}")
        except Exception as e:
            print(f"ERROR writing Markdown file {md_file_path}: {e}")
        return

    # Content previously in the loop now directly uses plc_info and target_plc_id
    markdown_lines.append(f"\n## PLC: {plc_info.get('name', target_plc_id)}")
    type_name = plc_info.get("type_name", "")
    order_num = plc_info.get("order_number", "")
    firmware = plc_info.get("firmware_version", "")
    if type_name and type_name != "":
        markdown_lines.append(f"- **Type Name:** `{type_name}`")
    if order_num and order_num != "":
        markdown_lines.append(f"- **Order Number:** `{order_num}`")
    if firmware and firmware != "":
        markdown_lines.append(f"- **Firmware:** `{firmware}`")

    # v32.5: Process PLC's local modules (modules in the same rack/structure as the PLC)
    plc_local_modules_io = []
    plc_parent_id = plc_info.get("parent_id")
    if plc_parent_id:
        # Find sibling modules in the same parent structure (rack)
        for dev_id, dev_info in project_data.get("devices", {}).items():
            if dev_info.get("parent_id") == plc_parent_id and dev_id != target_plc_id:
                # This is a sibling module of the PLC
                module_context = {
                    "id": dev_id,
                    "name": dev_info.get("name", dev_id),
                    "order_number": dev_info.get("order_number", ""),
                    "type_name": dev_info.get("type_name", ""),
                }
                module_ios = find_io_recursively(dev_id, project_data, module_context)
                if module_ios:
                    for addr_info in module_ios:
                        # Add to table data
                        length_bits = 0
                        siemens_addr = "FMT_ERROR"
                        try:
                            start_byte = int(addr_info.get("start", "0"))
                            length_bits = int(addr_info.get("length", "0"))
                            io_type = addr_info.get("type", "?")
                            length_bytes = math.ceil(length_bits / 8.0)
                            if length_bits > 0 and length_bytes == 0:
                                length_bytes = 1
                            end_byte = start_byte + length_bytes - 1
                            prefix = "P?"
                            if io_type.lower() == "input":
                                prefix = "EW"
                            elif io_type.lower() == "output":
                                prefix = "AW"
                            siemens_addr = f"{prefix} {start_byte}..{end_byte}"
                        except Exception:
                            siemens_addr = f"FMT_ERROR({addr_info.get('start', '?')},{addr_info.get('length', '?')})"

                        plc_local_modules_io.append(
                            {
                                "Network": "PLC Local Modules",
                                "Network Type": "Local I/O",
                                "Device Address": "Local",
                                "Device Name": f"PLC {plc_info.get('name', target_plc_id)}",
                                "Sub-Device": addr_info.get("module_name", "?"),
                                "Sub-Device OrderNo": addr_info.get(
                                    "module_order_number", "N/A"
                                ),
                                "Sub-Device Type": addr_info.get(
                                    "module_type_name", "N/A"
                                ),
                                "IO Type": addr_info.get("type", "?"),
                                "IO Address": siemens_addr,
                                "Number of Bits": length_bits,
                                "SortKey": (
                                    "PLC Local Modules",  # Network name for sorting
                                    [0],  # Device sort key (always first)
                                    (
                                        int(addr_info.get("module_pos", "9999"))
                                        if str(
                                            addr_info.get("module_pos", "9999")
                                        ).isdigit()
                                        else 9999
                                    ),
                                    addr_info.get("module_name", ""),
                                    addr_info.get("type", ""),
                                    (
                                        int(addr_info.get("start", "0"))
                                        if str(addr_info.get("start", "0")).isdigit()
                                        else float("inf")
                                    ),
                                ),
                            }
                        )

    # Add PLC local modules to the main IO list
    all_plc_io_for_table.extend(plc_local_modules_io)

    # Show PLC local modules in markdown
    if plc_local_modules_io:
        markdown_lines.append("\n- **Local I/O Modules (same rack as PLC):**")
        modules_by_name = {}
        for io_data in plc_local_modules_io:
            module_name = io_data["Sub-Device"]
            if module_name not in modules_by_name:
                modules_by_name[module_name] = []
            modules_by_name[module_name].append(io_data)

        for module_name in sorted(modules_by_name.keys()):
            module_ios = modules_by_name[module_name]
            first_io = module_ios[0]
            markdown_lines.append(
                f"  - **{module_name}** (Type: `{first_io['Sub-Device Type']}`, OrderNo: `{first_io['Sub-Device OrderNo']}`)"
            )
            for io_data in sorted(module_ios, key=lambda x: x["SortKey"]):
                markdown_lines.append(
                    f"    - `{io_data['IO Address']}` ({io_data['IO Type']}, {io_data['Number of Bits']} bits)"
                )

    plc_networks = plc_info.get("connected_networks", {})
    markdown_lines.append("\n- **Networks:**")
    if not plc_networks:
        markdown_lines.append(
            "  - *No network connections found associated with this PLC object.*"
        )
    else:
        sorted_network_items = sorted(
            plc_networks.items(),
            key=lambda item: project_data.get("networks", {})
            .get(item[0], {})
            .get("name", item[0]),
        )

        for net_id, plc_addr_on_net in sorted_network_items:
            net_info = project_data.get("networks", {}).get(net_id)
            if not net_info:
                markdown_lines.append(
                    f"  - !!! Error: Network info missing for ID {net_id} !!!"
                )
                continue

            markdown_lines.append(
                f"  - ### {net_info.get('name', net_id)} ({net_info.get('type', 'Unknown')})"
            )
            markdown_lines.append(f"    - PLC Address on this Net: `{plc_addr_on_net}`")
            markdown_lines.append(f"    - **Devices on Network:**")

            devices_on_this_net = net_info.get("devices_on_net", {})

            def sort_key(item):
                node_id, node_addr = item
                try:
                    parts = [int(p) for p in re.findall(r"\d+", node_addr)]
                    return parts
                except:
                    return [float("inf")]

            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)  # Use target_plc_id here

            other_device_items = sorted(
                [
                    (node_id, node_addr)
                    for node_id, node_addr in devices_on_this_net.items()
                    if node_id not in plc_interface_and_node_ids
                ],
                key=sort_key,
            )

            if not other_device_items:
                markdown_lines.append("      - *None (besides PLC interfaces)*")
            else:
                # --- Display Logic with Sibling IO Aggregation & Aesthetics ---
                for node_id, node_addr in other_device_items:
                    # v31: Initialize list for table data for the current device being processed
                    current_device_io_for_table = []

                    node_info = project_data.get("devices", {}).get(node_id)
                    if not node_info:
                        markdown_lines.append(
                            f"      - !!! Error: Node info missing for ID {node_id} Addr: {node_addr} !!!"
                        )
                        continue

                    interface_id = node_info.get("parent_id")
                    interface_info_dev = None  # Renamed to avoid conflict
                    actual_device_id = None
                    actual_device_info = None
                    rack_id = None
                    # rack_info = None # rack_info was not used
                    if interface_id:
                        interface_info_dev = project_data.get("devices", {}).get(
                            interface_id
                        )
                        if interface_info_dev:
                            actual_device_id = interface_info_dev.get("parent_id")
                            if actual_device_id:
                                actual_device_info = project_data.get(
                                    "devices", {}
                                ).get(actual_device_id)
                                if actual_device_info:
                                    potential_rack_id = actual_device_info.get(
                                        "parent_id"
                                    )
                                    if potential_rack_id:
                                        potential_rack_info = project_data.get(
                                            "devices", {}
                                        ).get(potential_rack_id)
                                        if potential_rack_info and (
                                            "Rack"
                                            in potential_rack_info.get("name", "")
                                            or potential_rack_info.get("position")
                                            is None
                                        ):
                                            rack_id = potential_rack_id
                                            # rack_info = potential_rack_info # Not used

                    display_info_title = (
                        actual_device_info
                        if actual_device_info
                        else (interface_info_dev if interface_info_dev else node_info)
                    )
                    display_id_title = (
                        actual_device_id
                        if actual_device_info
                        else (interface_id if interface_info_dev else node_id)
                    )

                    io_search_root_id = (
                        actual_device_id
                        if actual_device_info
                        else (interface_id if interface_info_dev else node_id)
                    )
                    io_search_root_info = project_data.get("devices", {}).get(
                        io_search_root_id
                    )

                    # Construct Title
                    display_name = display_info_title.get("name", display_id_title)
                    via_node_name = node_info.get("name", node_id)
                    title_str = f"#### {display_name}"
                    if display_id_title != node_id:
                        title_str += f" (via {via_node_name} @ `{node_addr}`)"
                    else:
                        title_str += f" (@ `{node_addr}`)"
                    markdown_lines.append(f"      - {title_str}")

                    # Display Basic Details
                    markdown_lines.append(f"        - Address (on net): `{node_addr}`")
                    type_name_disp = display_info_title.get("type_name", "")
                    order_num_disp = display_info_title.get("order_number", "")
                    pos_disp = display_info_title.get("position", "")
                    if type_name_disp and type_name_disp != "":
                        markdown_lines.append(
                            f"        - Type Name: `{type_name_disp}`"
                        )
                    if order_num_disp and order_num_disp != "":
                        markdown_lines.append(f"        - Order No: `{order_num_disp}`")
                    if pos_disp and pos_disp != "":
                        markdown_lines.append(
                            f"        - Pos (in parent): `{pos_disp}`"
                        )
                    ultimate_parent_id = rack_id
                    if not ultimate_parent_id and actual_device_info:
                        ultimate_parent_id = actual_device_info.get("parent_id")
                    if ultimate_parent_id and ultimate_parent_id != display_id_title:
                        ultimate_parent_info = project_data.get("devices", {}).get(
                            ultimate_parent_id
                        )
                        ultimate_parent_name = (
                            ultimate_parent_info.get("name", "?")
                            if ultimate_parent_info
                            else "?"
                        )
                        markdown_lines.append(
                            f"        - Parent Structure: `{ultimate_parent_name}`"
                        )

                    # --- IO Aggregation Logic (from v24) ---
                    aggregated_io_addresses = []
                    parent_structure_id = (
                        io_search_root_info.get("parent_id")
                        if io_search_root_info
                        else None
                    )
                    io_search_root_name_disp = (
                        io_search_root_info.get("name", "?")
                        if io_search_root_info
                        else "?"
                    )

                    if parent_structure_id:
                        parent_structure_info = project_data.get("devices", {}).get(
                            parent_structure_id
                        )
                        parent_structure_name = (
                            parent_structure_info.get("name", "?")
                            if parent_structure_info
                            else "?"
                        )
                        search_title = f"parent '{parent_structure_name}'"
                        sibling_found_io = False
                        for dev_scan_id, dev_scan_info in project_data.get(
                            "devices", {}
                        ).items():
                            if dev_scan_info.get("parent_id") == parent_structure_id:
                                # This dev_scan_info is the module
                                module_context_for_sibling = {
                                    "id": dev_scan_id,
                                    "name": dev_scan_info.get("name", dev_scan_id),
                                    "order_number": dev_scan_info.get(
                                        "order_number", ""
                                    ),
                                    "type_name": dev_scan_info.get("type_name", ""),
                                }
                                io_from_sibling = find_io_recursively(
                                    dev_scan_id,
                                    project_data,
                                    module_context_for_sibling,
                                )
                                if io_from_sibling:
                                    aggregated_io_addresses.extend(io_from_sibling)
                                    sibling_found_io = True
                        if (
                            not sibling_found_io and not aggregated_io_addresses
                        ):  # Only show message if list still empty
                            markdown_lines.append(
                                f"        - *No IO Addresses found in modules under {search_title} (ID: {parent_structure_id}).*"
                            )

                    elif io_search_root_id:
                        search_title = f"'{io_search_root_name_disp}'"
                        module_context_for_root = {
                            "id": io_search_root_id,
                            "name": io_search_root_info.get("name", io_search_root_id),
                            "order_number": io_search_root_info.get("order_number", ""),
                            "type_name": io_search_root_info.get("type_name", ""),
                        }
                        aggregated_io_addresses = find_io_recursively(
                            io_search_root_id, project_data, module_context_for_root
                        )
                        if not aggregated_io_addresses:
                            markdown_lines.append(
                                f"        - *No IO Addresses found in modules under {search_title} (ID: {io_search_root_id}).*"
                            )
                    else:
                        markdown_lines.append(
                            f"        - *Could not determine structure to search for IO addresses.*"
                        )
                    # --- End IO Aggregation ---

                    # Display aggregated IO Addresses with Siemens format (Cleaned)
                    if aggregated_io_addresses:
                        markdown_lines.append(
                            f"        - **IO Addresses (Aggregated from Structure):**"
                        )
                        sorted_agg_io = sorted(
                            aggregated_io_addresses,
                            key=lambda x: (
                                (
                                    int(x.get("module_pos", "9999"))
                                    if str(
                                        x.get("module_pos", "9999")
                                    ).isdigit()  # Ensure it's a string before isdigit
                                    else 9999
                                ),
                                x.get("module_name", ""),
                                x.get("type", ""),
                                (
                                    int(x.get("start", "0"))
                                    if str(
                                        x.get("start", "0")
                                    ).isdigit()  # Ensure it's a string
                                    else float("inf")
                                ),
                            ),
                        )
                        last_module_id_processed = (
                            None  # Use the actual module ID for grouping
                        )
                        for addr_info in sorted_agg_io:
                            current_module_id_for_grouping = addr_info.get("module_id")
                            if (
                                current_module_id_for_grouping
                                != last_module_id_processed
                            ):
                                module_name_disp = addr_info.get("module_name", "?")
                                module_type_name_disp = addr_info.get(
                                    "module_type_name", ""
                                )
                                module_order_num_disp = addr_info.get(
                                    "module_order_number", ""
                                )

                                module_line_parts = [f"**{module_name_disp}**"]
                                if (
                                    module_type_name_disp
                                    and module_type_name_disp != ""
                                ):
                                    module_line_parts.append(
                                        f"Type: `{module_type_name_disp}`"
                                    )
                                if (
                                    module_order_num_disp
                                    and module_order_num_disp != ""
                                ):
                                    module_line_parts.append(
                                        f"OrderNo: `{module_order_num_disp}`"
                                    )

                                # Removed (Pos: ...) from this line as requested
                                markdown_lines.append(
                                    f"          - {', '.join(module_line_parts)}"
                                )
                                last_module_id_processed = (
                                    current_module_id_for_grouping
                                )

                            # --- Siemens IO Formatting (from v25.1 - keep fixes) ---
                            io_type = addr_info.get("type", "?")
                            start_str = addr_info.get("start", "?")
                            length_str = addr_info.get("length", "?")
                            # area_str = addr_info.get("area", "?") # Not used in final output string
                            siemens_addr = f"FMT_ERROR"  # Default error
                            length_bits = 0
                            try:
                                start_byte = int(start_str)
                                length_bits = int(length_str)
                                length_bytes = math.ceil(
                                    length_bits / 8.0
                                )  # Use float division
                                if length_bits > 0 and length_bytes == 0:
                                    length_bytes = 1  # Handle len < 8 bits
                                end_byte = start_byte + length_bytes - 1
                                prefix = "P?"
                                if io_type.lower() == "input":
                                    prefix = "EW"
                                elif io_type.lower() == "output":
                                    prefix = "AW"
                                siemens_addr = f"{prefix} {start_byte}..{end_byte}"
                            except Exception:
                                siemens_addr = f"FMT_ERROR({start_str},{length_str})"
                            # v31: Collect data for the summary table (Corrected Indentation)
                            current_device_io_for_table.append(
                                {
                                    "Network": net_info.get("name", net_id),
                                    "Network Type": net_info.get("type", "Unknown"),
                                    "Device Address": node_addr,
                                    "Device Name": display_name,  # Main device name
                                    "Sub-Device": addr_info.get(
                                        "module_name", "?"
                                    ),  # Module name
                                    "Sub-Device OrderNo": addr_info.get(
                                        "module_order_number", ""
                                    ),
                                    "Sub-Device Type": addr_info.get(
                                        "module_type_name", ""
                                    ),
                                    "IO Type": io_type,
                                    "IO Address": siemens_addr,
                                    "Number of Bits": length_bits,
                                    "SortKey": (  # Add a sort key for the table
                                        net_info.get("name", net_id),
                                        sort_key(
                                            (node_id, node_addr)
                                        ),  # Reuse the device sort key
                                        (
                                            int(addr_info.get("module_pos", "9999"))
                                            if str(
                                                addr_info.get("module_pos", "9999")
                                            ).isdigit()
                                            else 9999
                                        ),
                                        addr_info.get("module_name", ""),
                                        io_type,
                                        (
                                            int(addr_info.get("start", "0"))
                                            if str(
                                                addr_info.get("start", "0")
                                            ).isdigit()
                                            else float("inf")
                                        ),
                                    ),
                                }
                            )

                            markdown_lines.append(
                                f"            - `{siemens_addr}` (Len={length_bits} bits)"
                            )
                            # --- End Siemens IO Formatting ---

                    # IO Connections logic
                    links_from = project_data.get("links_by_source", {}).get(
                        display_id_title, []
                    )
                    links_to = project_data.get("links_by_target", {}).get(
                        display_id_title, []
                    )
                    io_conns = []
                    for link in links_from:
                        if "channel" in link["source_suffix"].lower():
                            target_str = f"{link.get('target_device_name', link['target_id'])}:{link['target_suffix']}"
                            if link["target_id"] == display_id_title:
                                target_str = link["target_suffix"]
                            io_conns.append(
                                f"`{link['source_suffix']}` → `{target_str}`"
                            )
                    for link in links_to:
                        if "channel" in link["target_suffix"].lower():
                            source_str = f"{link.get('source_device_name', link['source_id'])}:{link['source_suffix']}"
                            if link["source_id"] == display_id_title:
                                source_str = link["source_suffix"]
                            io_conns.append(
                                f"`{source_str}` → `{link['target_suffix']}`"
                            )
                    if io_conns:
                        markdown_lines.append(
                            f"        - **IO Connections (Channels):**"
                        )
                        for conn in sorted(list(set(io_conns))):
                            markdown_lines.append(f"          - {conn}")
                    # v31: Add collected IO for this device to the main list
                    all_plc_io_for_table.extend(current_device_io_for_table)
                    markdown_lines.append("")  # Spacing
                # --- *** END Display Logic *** ---

    try:
        # Re-open the file in write mode to include the tree structure (without the table)
        with open(md_file_path, "w", encoding="utf-8") as f:
            f.write("\n".join(markdown_lines))
        print(f"Markdown tree summary written to: {md_file_path}")

        # Generate the separate Hardware.md with the IO summary table
        if all_plc_io_for_table:
            hardware_file_path = generate_io_summary_file(
                all_plc_io_for_table,
                md_file_path,
                plc_name_for_title,
                project_data,
                output_root_path,
            )
            print(f"IO summary table generated in separate file: {hardware_file_path}")

    except Exception as e:
        print(f"ERROR writing Markdown file {md_file_path}: {e}")
        traceback.print_exc()


# --- generate_io_upward_tree function (Unchanged from v23) ---
def generate_io_upward_tree(project_data, md_file_path):
    """(v23) Generates a debug tree starting from IO addresses upwards."""
    markdown_lines = ["# IO Address Upward Connection Trace (Debug v23)", ""]
    if not project_data or not project_data.get("devices"):
        markdown_lines.append("*No device data found.*")
        try:
            with open(md_file_path, "w", encoding="utf-8") as f:
                f.write("\n".join(markdown_lines))
            print(f"\nIO upward debug tree written to: {md_file_path}")
        except Exception as e:
            print(f"ERROR writing IO upward debug tree file {md_file_path}: {e}")
        return
    node_to_network_map = {}
    for net_id, net_info in project_data.get("networks", {}).items():
        net_name = net_info.get("name", "?")
        for node_id, node_addr in net_info.get("devices_on_net", {}).items():
            node_to_network_map[node_id] = (net_id, net_name, node_addr)
    devices_with_io = []
    for dev_id, dev_info in project_data.get("devices", {}).items():
        if dev_info.get("io_addresses"):
            devices_with_io.append((dev_id, dev_info))
    if not devices_with_io:
        markdown_lines.append("*No devices with defined IO Addresses found.*")
    else:
        markdown_lines.append(
            f"Found {len(devices_with_io)} device(s)/module(s) with IO addresses. Tracing connections upwards:\n"
        )
        devices_with_io.sort(
            key=lambda item: (
                (
                    int(item[1].get("position", "9999"))
                    if item[1].get("position", "9999").isdigit()
                    else 9999
                ),
                item[1].get("name", ""),
            )
        )
        for dev_id, dev_info in devices_with_io:
            markdown_lines.append(
                f"## IO Module: {dev_info.get('name', dev_id)} (ID: {dev_id})"
            )
            markdown_lines.append(f"- Position: {dev_info.get('position', '')}")
            markdown_lines.append("- IO Addresses:")
            for addr in sorted(
                dev_info["io_addresses"],
                key=lambda x: (
                    x.get("type", ""),
                    (
                        int(x.get("start", "0"))
                        if x.get("start", "0").isdigit()
                        else float("inf")
                    ),
                ),
            ):
                markdown_lines.append(
                    f"  - `{addr.get('type','?').ljust(6)} Start={addr.get('start','?').ljust(4)} Len={addr.get('length','?').ljust(3)}` (Area: {addr.get('area','?')})"
                )
            markdown_lines.append("- Upward Path:")
            current_id = dev_id
            current_info = dev_info
            indent = "  "
            path_found = False
            ancestor_limit = 15
            count = 0
            while current_id and count < ancestor_limit:
                ancestor_name = current_info.get("name", "?") if current_info else "?"
                ancestor_pos = current_info.get("position", "") if current_info else ""
                markdown_lines.append(
                    f"{indent}└─ {ancestor_name} (ID: {current_id}, Pos: {ancestor_pos})"
                )
                if current_id in node_to_network_map:
                    net_id, net_name, node_addr = node_to_network_map[current_id]
                    markdown_lines.append(f"{indent}   └─ **Network Connection Point**")
                    markdown_lines.append(
                        f"{indent}      - Node: {ancestor_name} (ID: {current_id})"
                    )
                    markdown_lines.append(
                        f"{indent}      - Network: {net_name} (ID: {net_id})"
                    )
                    markdown_lines.append(f"{indent}      - Address: `{node_addr}`")
                    plc_connection_found = False
                    for plc_id_check, plc_info_check in project_data.get(
                        "plcs", {}
                    ).items():
                        if net_id in plc_info_check.get("connected_networks", {}):
                            markdown_lines.append(
                                f"{indent}      - **Network associated with PLC:** {plc_info_check.get('name','?')} (ID: {plc_id_check})"
                            )
                            plc_connection_found = True
                            break
                    if not plc_connection_found:
                        markdown_lines.append(
                            f"{indent}      - *Network not directly associated with a known PLC in data.*"
                        )
                    path_found = True
                    break
                if current_id in project_data.get("plcs", {}):
                    markdown_lines.append(f"{indent}   └─ **Is PLC:** {ancestor_name}")
                    path_found = True
                    break
                parent_id = current_info.get("parent_id") if current_info else None
                if parent_id:
                    current_info = project_data.get("devices", {}).get(parent_id)
                    if not current_info:
                        markdown_lines.append(
                            f"{indent}   └─ Parent ID {parent_id} not found. Stopping trace."
                        )
                        break
                    current_id = parent_id
                    indent += "  "
                else:
                    markdown_lines.append(
                        f"{indent}   └─ Reached top level (no parent)."
                    )
                    break
                count += 1
                if count >= ancestor_limit:
                    markdown_lines.append(
                        f"{indent}   └─ Reached ancestor limit. Stopping trace."
                    )
            if not path_found:
                markdown_lines.append(
                    f"{indent}└─ *Could not trace path to a known Network Node or PLC.*"
                )
            markdown_lines.append("")
    try:
        with open(md_file_path, "w", encoding="utf-8") as f:
            f.write("\n".join(markdown_lines))
        print(f"\nIO upward debug tree written to: {md_file_path}")
    except Exception as e:
        print(f"ERROR writing IO upward debug tree file {md_file_path}: {e}")


# --- extract_and_save_global_outputs function (Refactored from process_aml_file) ---
def extract_and_save_global_outputs(
    aml_file_path, json_output_path, md_upward_output_path
):
    """Extracts data from AML, saves global JSON and IO upward tree, returns project_data."""
    # (Unchanged)
    print(f"Processing AML file: {aml_file_path}")
    if not os.path.exists(aml_file_path):
        print(f"ERROR: Input AML file not found at {aml_file_path}")
        return
    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)  # v15 extraction
        print(f"Generating JSON output: {json_output_path}")
        try:
            with open(json_output_path, "w", encoding="utf-8") as f:
                json.dump(project_data, f, indent=4, default=str)
            print(f"JSON data written successfully.")
        except Exception as e:
            print(f"ERROR writing JSON file {json_output_path}: {e}")
            traceback.print_exc()

        # Generate and save the IO upward tree (global)
        generate_io_upward_tree(project_data, md_upward_output_path)
        return project_data
    except ET.LxmlError as xml_err:
        print(f"ERROR parsing XML file {aml_file_path} with lxml: {xml_err}")
        traceback.print_exc()
        return None
    except Exception as e:
        print(f"ERROR processing AML file {aml_file_path}: {e}")
        traceback.print_exc()
        return None


def find_aml_file_in_dir(directory):
    """Finds a single AML file in the given directory and returns its path."""
    try:
        aml_files = list(Path(directory).glob("*.aml"))

        if len(aml_files) == 1:
            print(f"Automatically found AML file: {aml_files[0]}")
            return aml_files[0]
        elif len(aml_files) == 0:
            print(f"INFO: No .aml file found in '{directory}'.")
            return None
        else:
            print(f"WARNING: Multiple .aml files found in '{directory}':")
            for f in aml_files:
                print(f" - {f.name}")
            return None
    except Exception as e:
        print(f"Error when searching for AML file in '{directory}': {e}")
        return None


def select_cax_file(initial_dir=None):  # Add initial_dir parameter
    """Opens a dialog to select a CAx (XML) export file, starting in the specified directory."""
    root = tk.Tk()
    root.withdraw()
    file_path = filedialog.askopenfilename(
        title="Select CAx Export File (AML)",
        filetypes=[("AML Files", "*.aml"), ("All Files", "*.*")],  # Added AML
        initialdir=initial_dir,  # Set the initial directory
    )
    root.destroy()
    if not file_path:
        print("No CAx file selected. Exiting.")
        sys.exit(0)
    return file_path


def select_output_directory():
    """Opens a dialog to select the output directory."""
    root = tk.Tk()
    root.withdraw()
    dir_path = filedialog.askdirectory(
        title="Select Output Directory for JSON and MD files"  # Updated title slightly
    )
    root.destroy()
    if not dir_path:
        print("No output directory selected. Exiting.")
        sys.exit(0)
    return dir_path


def sanitize_filename(name):
    """Sanitizes a string to be used as a valid filename or directory name."""
    name = str(name)  # Ensure it's a string
    name = re.sub(r'[<>:"/\\|?*]', "_", name)  # Replace forbidden characters
    name = re.sub(
        r"\s+", "_", name
    )  # Replace multiple whitespace with single underscore
    name = name.strip("._")  # Remove leading/trailing dots or underscores
    return name if name else "Unnamed_Device"


# --- Main Execution ---
if __name__ == "__main__":
    try:
        configs = load_configuration()
        working_directory = configs.get("working_directory")
        level2_configs = configs.get("level2", {})
        aml_exp_directory = level2_configs.get("aml_exp_directory")
        resultados_exp_directory = level2_configs.get("resultados_exp_directory")
    except Exception as e:
        print(f"Warning: Could not load configuration (frontend not running): {e}")
        configs = {}
        working_directory = None
        aml_exp_directory = None
        resultados_exp_directory = None

    script_version = "v32.8 - Use empty strings instead of N/A"  # Updated version
    print(
        f"--- AML (CAx Export) to Hierarchical JSON and Obsidian MD Converter ({script_version}) ---"
    )

    # Determine the CAx directory for input AML
    cax_directory = None
    if working_directory and aml_exp_directory and os.path.isdir(working_directory):
        cax_directory = os.path.join(working_directory, aml_exp_directory)
        print(f"Using configured CAx directory for input: {cax_directory}")
        # Ensure the directory exists
        os.makedirs(cax_directory, exist_ok=True)
    else:
        print("Working directory or aml_exp_directory not configured or invalid.")
        print("Using .debug directory as fallback for direct script execution.")

        # Fallback to .debug directory under script location
        script_dir = os.path.dirname(os.path.abspath(__file__))
        debug_dir = os.path.join(script_dir, ".debug")

        # Create .debug directory if it doesn't exist
        os.makedirs(debug_dir, exist_ok=True)
        cax_directory = debug_dir
        print(f"Using debug directory: {cax_directory}")

    # Determine results directory for outputs
    results_dir = None
    if (
        working_directory
        and resultados_exp_directory
        and os.path.isdir(working_directory)
    ):
        results_dir = os.path.join(working_directory, resultados_exp_directory)
        print(f"Using configured results directory for output: {results_dir}")
    else:
        print(
            "WARNING: resultados_exp_directory not set. Using CAx directory for all outputs."
        )
        results_dir = cax_directory  # Fallback
    os.makedirs(results_dir, exist_ok=True)

    # The cax_directory is for input and intermediate files.
    # The results_dir is for final output files.
    print(f"Using Directory for Input: {cax_directory}")
    print(f"Using Directory for Output Results: {results_dir}")

    # 1. Try to find a single AML file automatically
    cax_file_path = find_aml_file_in_dir(cax_directory)

    # If not found or multiple are found, fall back to manual selection
    if not cax_file_path:
        print("Could not find a unique AML file. Please select one manually.")
        cax_file_path = select_cax_file(initial_dir=cax_directory)

    # Convert paths to Path objects
    input_path = Path(cax_file_path)
    output_path = Path(cax_directory)  # For intermediate files
    results_path = Path(results_dir)  # For final result files

    # Check if input file exists
    if not input_path.is_file():
        print(f"ERROR: Input file '{input_path}' not found or is not a file.")
        sys.exit(1)

    # Ensure output directory exists (redundant if working_directory is valid, but safe)
    output_path.mkdir(parents=True, exist_ok=True)

    # Construct output file paths
    output_json_file = output_path / input_path.with_suffix(".hierarchical.json").name
    output_md_upward_file = output_path / input_path.with_name(
        f"{input_path.stem}_IO_Upward_Debug.md"
    )

    print(f"Input AML: {input_path.resolve()}")
    print(f"Intermediate Output Directory: {output_path.resolve()}")
    print(f"Results Directory: {results_path.resolve()}")
    print(f"Output JSON: {output_json_file.resolve()}")
    print(f"Output IO Debug Tree MD: {output_md_upward_file.resolve()}")

    # Process the AML file to get project_data and save global files
    project_data = extract_and_save_global_outputs(
        str(input_path),
        str(output_json_file),
        str(output_md_upward_file),
    )

    if project_data:
        # Now, generate the hardware tree per PLC
        if not project_data.get("plcs"):
            print(
                "\nNo PLCs found in the project data. Cannot generate PLC-specific hardware trees."
            )
        else:
            print(
                f"\nFound {len(project_data['plcs'])} PLC(s). Generating individual hardware trees..."
            )
            for plc_id, plc_data_for_plc in project_data.get("plcs", {}).items():
                plc_name_original = plc_data_for_plc.get("name", plc_id)
                plc_name_sanitized = sanitize_filename(plc_name_original)

                plc_doc_dir = results_path / plc_name_sanitized / "Documentation"
                plc_doc_dir.mkdir(parents=True, exist_ok=True)

                hardware_tree_md_filename = f"{input_path.stem}_Hardware_Tree.md"
                output_plc_md_file = plc_doc_dir / hardware_tree_md_filename

                print(
                    f"  Generating Hardware Tree for PLC '{plc_name_original}' (ID: {plc_id}) at: {output_plc_md_file.resolve()}"
                )
                # Pass results_path as the root directory for Hardware.md placement
                generate_markdown_tree(
                    project_data, str(output_plc_md_file), plc_id, str(results_path)
                )

                # Generate Excel IO report for this PLC
                excel_io_filename = f"{input_path.stem}_IO_Report.xlsx"
                output_excel_file = plc_doc_dir / excel_io_filename
                print(
                    f"  Generating Excel IO Report for PLC '{plc_name_original}' (ID: {plc_id}) at: {output_excel_file.resolve()}"
                )
                generate_io_excel_report(
                    project_data, str(output_excel_file), plc_id, str(results_path)
                )
    else:
        print(
            "\nFailed to process AML data. Halting before generating PLC-specific trees."
        )

    print("\nScript finished.")
