# ToUpload/parsers/parse_lad_fbd.py
# -*- coding: utf-8 -*-
from lxml import etree
from collections import defaultdict
import copy
import traceback

# Importar desde las utilidades del parser
from .parser_utils import (
    ns,
    parse_access,
    parse_part,
    parse_call,
    get_multilingual_text,
)

# Sufijo usado en x2 para identificar instrucciones procesadas (útil para EN/ENO)
SCL_SUFFIX = "_sympy_processed"  # Asumimos que este es el sufijo de x2


def parse_lad_fbd_network(network_element):
    """
    Parsea una red LAD/FBD/GRAPH, extrae lógica y añade conexiones EN/ENO implícitas.
    Devuelve un diccionario representando la red para el JSON.
    """
    if network_element is None:
        return {
            "id": "ERROR",
            "title": "Invalid Network Element",
            "logic": [],
            "error": "Input element was None",
        }

    network_id = network_element.get("ID")
    # Usar get_multilingual_text de utils
    title_element = network_element.xpath(
        ".//iface:MultilingualText[@CompositionName='Title']", namespaces=ns
    )
    network_title = (
        get_multilingual_text(title_element[0])
        if title_element
        else f"Network {network_id}"
    )
    comment_element = network_element.xpath(
        "./ObjectList/MultilingualText[@CompositionName='Comment']", namespaces=ns
    )  # OJO: Path relativo a CompileUnit?
    if not comment_element:  # Intentar path alternativo si el anterior falla
        comment_element = network_element.xpath(
            ".//MultilingualText[@CompositionName='Comment']", namespaces=ns
        )  # Más genérico dentro de la red
    network_comment = (
        get_multilingual_text(comment_element[0]) if comment_element else ""
    )

    # --- Determinar Lenguaje (ya que este parser maneja varios) ---
    network_lang = "Unknown"
    attr_list_net = network_element.xpath("./AttributeList")
    if attr_list_net:
        lang_node_net = attr_list_net[0].xpath("./ProgrammingLanguage/text()")
        if lang_node_net:
            network_lang = lang_node_net[0].strip()

    # --- Buscar FlgNet ---
    # Paso 1: localizar el nodo <NetworkSource> (sin importar namespace)
    network_source_node = network_element.xpath(".//*[local-name()='NetworkSource']")

    flgnet = None
    if network_source_node:
        # Buscar FlgNet dentro del NetworkSource
        flgnet_list = network_source_node[0].xpath(".//*[local-name()='FlgNet']")
        if flgnet_list:
            flgnet = flgnet_list[0]

    # Paso 2: si no se encontró, intentar buscar FlgNet directamente en el CompileUnit
    if flgnet is None:
        flgnet_list = network_element.xpath(".//*[local-name()='FlgNet']")
        if flgnet_list:
            flgnet = flgnet_list[0]

    if flgnet is None:
        return {
            "id": network_id,
            "title": network_title,
            "comment": network_comment,
            "language": network_lang,
            "logic": [],
            "error": "FlgNet not found inside NetworkSource or CompileUnit",
        }

    # 1. Parse Access, Parts, Calls (usan utils)
    access_map = {}
    # Corregir XPath para buscar Access dentro de FlgNet/Parts
    for acc in flgnet.xpath(".//flg:Parts/flg:Access", namespaces=ns):
        acc_info = parse_access(acc)
        if acc_info and acc_info.get("uid") and "error" not in acc_info.get("type", ""):
            access_map[acc_info["uid"]] = acc_info
        elif acc_info:
            print(
                f"Advertencia: Ignorando Access inválido o con error UID={acc_info.get('uid')} en red {network_id}"
            )

    parts_and_calls_map = {}
    # Corregir XPath para buscar Part y Call dentro de FlgNet/Parts
    instruction_elements = flgnet.xpath(
        ".//flg:Parts/flg:Part | .//flg:Parts/flg:Call", namespaces=ns
    )
    for element in instruction_elements:
        parsed_info = None
        tag_name = etree.QName(element.tag).localname
        if tag_name == "Part":
            parsed_info = parse_part(element)  # Usa utils
        elif tag_name == "Call":
            parsed_info = parse_call(element)  # Usa utils

        if (
            parsed_info
            and parsed_info.get("uid")
            and "error" not in parsed_info.get("type", "")
        ):
            parts_and_calls_map[parsed_info["uid"]] = parsed_info
        elif parsed_info:
            # Si parse_call/parse_part devolvió error, lo guardamos para tener el UID
            print(
                f"Advertencia: {tag_name} con error UID={parsed_info.get('uid')} en red {network_id}. Error: {parsed_info.get('error')}"
            )
            parts_and_calls_map[parsed_info["uid"]] = (
                parsed_info  # Guardar aunque tenga error
            )

    # 2. Parse Wires (lógica compleja, mantener aquí)
    wire_connections = defaultdict(list)  # destination -> [source1, source2]
    source_connections = defaultdict(list)  # source -> [dest1, dest2]
    eno_outputs = defaultdict(list)
    qname_powerrail = etree.QName(ns["flg"], "Powerrail")
    qname_identcon = etree.QName(
        ns["flg"], "IdentCon"
    )  # Conexión a/desde Access (variable/constante)
    qname_namecon = etree.QName(
        ns["flg"], "NameCon"
    )  # Conexión a/desde Part/Call (pin con nombre)
    qname_openbranch = etree.QName(
        ns["flg"], "Openbranch"
    )  # Rama abierta (normalmente ignorada o tratada como TRUE?)
    qname_opencon = etree.QName(
        ns["flg"], "OpenCon"
    )  # Conexión abierta (pin no conectado)

    # Corregir XPath para buscar Wire dentro de FlgNet/Wires
    for wire in flgnet.xpath(".//flg:Wires/flg:Wire", namespaces=ns):
        children = wire.getchildren()
        if len(children) < 2:
            continue  # Necesita al menos origen y destino

        source_elem = children[0]
        source_uid, source_pin = None, None

        # Determinar origen
        if source_elem.tag == qname_powerrail:
            source_uid, source_pin = "POWERRAIL", "out"
        elif source_elem.tag == qname_identcon:  # Origen es una variable/constante
            source_uid = source_elem.get("UId")
            source_pin = "value"  # Salida implícita de un Access
        elif source_elem.tag == qname_namecon:  # Origen es pin de instrucción
            source_uid = source_elem.get("UId")
            source_pin = source_elem.get("Name")
        elif source_elem.tag == qname_openbranch:
            # ¿Cómo manejar OpenBranch como fuente? Podría ser TRUE o una condición OR implícita
            source_uid = "OPENBRANCH_" + wire.get(
                "UId", "Unknown"
            )  # UID único para la rama
            source_pin = "out"
            print(
                f"Advertencia: OpenBranch encontrado como fuente en Wire UID={wire.get('UId')} (Red {network_id}). Tratando como fuente especial."
            )
            # No lo añadimos a parts_and_calls_map, get_sympy_representation necesitará manejarlo
        # Ignorar OpenCon como fuente (no tiene sentido)
        if source_uid is None or source_pin is None:
            # print(f"Advertencia: Fuente de wire inválida o no soportada: {source_elem.tag} en Wire UID={wire.get('UId')}")
            continue

        source_info = (source_uid, source_pin)

        # Procesar destinos
        for dest_elem in children[1:]:
            dest_uid, dest_pin = None, None

            if (
                dest_elem.tag == qname_identcon
            ):  # Destino es una variable/constante (asignación)
                dest_uid = dest_elem.get("UId")
                dest_pin = "value"  # Entrada implícita de un Access
            elif dest_elem.tag == qname_namecon:  # Destino es pin de instrucción
                dest_uid = dest_elem.get("UId")
                dest_pin = dest_elem.get("Name")
            # Ignorar Powerrail, OpenBranch, OpenCon como destinos válidos de conexión lógica principal

            if dest_uid is not None and dest_pin is not None:
                dest_key = (dest_uid, dest_pin)
                if source_info not in wire_connections[dest_key]:
                    wire_connections[dest_key].append(source_info)

                # Mapa inverso: source -> list of destinations
                source_key = (source_uid, source_pin)
                dest_info = (dest_uid, dest_pin)
                if dest_info not in source_connections[source_key]:
                    source_connections[source_key].append(dest_info)

                # Trackear salidas ENO específicamente si la fuente es una instrucción
                if source_pin == "eno" and source_uid in parts_and_calls_map:
                    if dest_info not in eno_outputs[source_uid]:
                        eno_outputs[source_uid].append(dest_info)

    # 3. Build Initial Logic Structure (incorporando errores)
    all_logic_steps = {}
    # Lista de tipos funcionales (usados para inferencia EN)
    # Estos son los tipos *originales* de las instrucciones
    functional_block_types = [
        "Move",
        "Add",
        "Sub",
        "Mul",
        "Div",
        "Mod",
        "Convert",
        "Call",  # Call ya está aquí
        "TON",
        "TOF",
        "TP",
        "CTU",
        "CTD",
        "CTUD",
        "BLKMOV",  # Añadidos
        "Se",
        "Sd",  # Estos son tipos LAD que se mapearán a timers SCL
    ]
    # Lista de generadores RLO (usados para inferencia EN)
    rlo_generators = [
        "Contact",
        "O",
        "Eq",
        "Ne",
        "Gt",
        "Lt",
        "Ge",
        "Le",
        "And",
        "Xor",
        "PBox",
        "NBox",
        "Not",
    ]

    # Iterar sobre UIDs válidos (los que se pudieron parsear, aunque sea con error)
    valid_instruction_uids = list(parts_and_calls_map.keys())

    for instruction_uid in valid_instruction_uids:
        instruction_info = parts_and_calls_map[instruction_uid]
        # Hacer copia profunda para no modificar el mapa original
        instruction_repr = copy.deepcopy(instruction_info)
        instruction_repr["instruction_uid"] = instruction_uid  # Asegurar UID
        instruction_repr["inputs"] = {}
        instruction_repr["outputs"] = {}

        # Si la instrucción ya tuvo un error de parseo, añadirlo aquí
        if "error" in instruction_info:
            instruction_repr["parsing_error"] = instruction_info["error"]
            # No intentar poblar inputs/outputs si el parseo base falló
            all_logic_steps[instruction_uid] = instruction_repr
            continue

        original_type = instruction_repr.get("type", "")  # Tipo de la instrucción

        # --- Poblar Entradas ---
        # Lista base de pines posibles (podría obtenerse de XSDs o dinámicamente)
        possible_input_pins = set(["en", "in", "in1", "in2", "pre"])
        
        # **NUEVO: Generar pines dinámicamente para compuertas OR/AND basándose en Cardinality**
        if original_type in ["O", "And"]:  # Compuertas lógicas
            cardinality = instruction_info.get("template_values", {}).get("Card")
            if cardinality:
                try:
                    num_inputs = int(cardinality)
                    # Generar pines in1, in2, ..., inN
                    for i in range(1, num_inputs + 1):
                        possible_input_pins.add(f"in{i}")
                    print(f"INFO: Compuerta {original_type} UID {instruction_uid} con cardinalidad {num_inputs} - generando pines in1...in{num_inputs}")
                except (ValueError, TypeError):
                    print(f"Advertencia: Cardinalidad inválida '{cardinality}' para {original_type} UID {instruction_uid}")
                    # Fallback a pines estándar
                    possible_input_pins.update(["in1", "in2"])
            else:
                # Sin cardinalidad explícita, usar pines estándar
                possible_input_pins.update(["in1", "in2"])
        
        # Añadir pines dinámicamente basados en el tipo de instrucción
        if original_type in ["Contact", "Coil", "SCoil", "RCoil", "SdCoil"]:
            possible_input_pins.add("operand")
        elif original_type in [
            "Add",
            "Sub",
            "Mul",
            "Div",
            "Mod",
            "Eq",
            "Ne",
            "Gt",
            "Lt",
            "Ge",
            "Le",
        ]:
            possible_input_pins.update(["in1", "in2"])
        elif original_type in ["TON", "TOF", "TP"]:
            possible_input_pins.update(["IN", "PT"])  # Pines SCL
        elif original_type in ["Se", "Sd"]:
            possible_input_pins.update(["s", "tv", "timer"])  # Pines LAD
        elif original_type in ["CTU", "CTD", "CTUD"]:
            possible_input_pins.update(["CU", "CD", "R", "LD", "PV"])  # Pines SCL/LAD
        elif original_type in ["PBox", "NBox"]:
            possible_input_pins.update(
                ["bit", "clk", "in"]
            )  # PBox/NBox usa 'in' y 'bit'
        elif original_type == "BLKMOV":
            possible_input_pins.add("SRCBLK")
        elif original_type == "Move":
            possible_input_pins.add("in")
        elif original_type == "Convert":
            possible_input_pins.add("in")
        elif original_type == "Call":
            # Para Calls, los nombres de los parámetros reales se definen en el XML
            # El Xpath busca Parameter DENTRO de CallInfo, que está DENTRO de Call
            call_xml_element_list = flgnet.xpath(
                f".//flg:Parts/flg:Call[@UId='{instruction_uid}']", namespaces=ns
            )
            if call_xml_element_list:
                call_xml_element = call_xml_element_list[0]
                call_info_node_list = call_xml_element.xpath(
                    "./flg:CallInfo", namespaces=ns
                )
                if call_info_node_list:
                    call_param_names = call_info_node_list[0].xpath(
                        "./flg:Parameter/@Name", namespaces=ns
                    )
                    possible_input_pins.update(call_param_names)
                    # print(f"DEBUG Call UID={instruction_uid}: Params={call_param_names}")
                else:  # Fallback si no hay namespace (menos probable)
                    call_info_node_list_no_ns = call_xml_element.xpath("./CallInfo")
                    if call_info_node_list_no_ns:
                        possible_input_pins.update(
                            call_info_node_list_no_ns[0].xpath("./Parameter/@Name")
                        )

        # Iterar sobre pines posibles y buscar conexiones
        for pin_name in possible_input_pins:
            dest_key = (instruction_uid, pin_name)
            if dest_key in wire_connections:
                sources_list = wire_connections[dest_key]
                input_sources_repr = []
                for source_uid, source_pin in sources_list:
                    source_repr = None
                    if source_uid == "POWERRAIL":
                        source_repr = {"type": "powerrail"}
                    elif source_uid.startswith("OPENBRANCH_"):
                        source_repr = {
                            "type": "openbranch",
                            "uid": source_uid,
                        }  # Fuente especial
                    elif source_uid in access_map:
                        source_repr = copy.deepcopy(access_map[source_uid])
                    elif source_uid in parts_and_calls_map:
                        source_instr_info = parts_and_calls_map[source_uid]
                        source_repr = {
                            "type": "connection",
                            "source_instruction_type": source_instr_info.get(
                                "type", "Unknown"
                            ),  # Usar tipo base
                            "source_instruction_uid": source_uid,
                            "source_pin": source_pin,
                        }
                    else:
                        # Fuente desconocida (ni Access, ni Part/Call válido)
                        print(
                            f"Advertencia: Fuente desconocida UID={source_uid} conectada a {instruction_uid}.{pin_name}"
                        )
                        source_repr = {"type": "unknown_source", "uid": source_uid}
                    input_sources_repr.append(source_repr)

                # Guardar la representación de la entrada (lista o dict)
                instruction_repr["inputs"][pin_name] = (
                    input_sources_repr[0]
                    if len(input_sources_repr) == 1
                    else input_sources_repr
                )

        # --- Poblar Salidas (simplificado: solo conexiones a Access) ---
        possible_output_pins = set(
            [
                "out",
                "out1",
                "Q",
                "q",
                "eno",
                "RET_VAL",
                "DSTBLK",
                "rt",
                "cv",
                "QU",
                "QD",
                "ET",  # Añadir pines de salida estándar SCL
            ]
        )
        if original_type == "BLKMOV":
            possible_output_pins.add("DSTBLK")
        if (
            original_type == "Call"
        ):  # Para Calls, las salidas dependen del bloque llamado
            call_xml_element_list = flgnet.xpath(
                f".//flg:Parts/flg:Call[@UId='{instruction_uid}']", namespaces=ns
            )
            if call_xml_element_list:
                call_info_node_list = call_xml_element_list[0].xpath(
                    "./flg:CallInfo", namespaces=ns
                )
                if call_info_node_list:
                    # Buscar parámetros con Section="Output" o "InOut" o "Return"
                    output_param_names = call_info_node_list[0].xpath(
                        "./flg:Parameter[@Section='Output' or @Section='InOut' or @Section='Return']/@Name",
                        namespaces=ns,
                    )
                    possible_output_pins.update(output_param_names)

        for pin_name in possible_output_pins:
            source_key = (instruction_uid, pin_name)
            if source_key in source_connections:
                if pin_name not in instruction_repr["outputs"]:
                    instruction_repr["outputs"][pin_name] = []
                for dest_uid, dest_pin in source_connections[source_key]:
                    if (
                        dest_uid in access_map
                    ):  # Solo registrar si va a una variable/constante
                        dest_operand_copy = copy.deepcopy(access_map[dest_uid])
                        if (
                            dest_operand_copy
                            not in instruction_repr["outputs"][pin_name]
                        ):
                            instruction_repr["outputs"][pin_name].append(
                                dest_operand_copy
                            )

        all_logic_steps[instruction_uid] = instruction_repr

    # 4. Inferencia EN (modificado para usar tipos originales)
    processed_blocks_en_inference = set()
    try:
        # Ordenar UIDs numéricamente si es posible
        sorted_uids_for_en = sorted(
            all_logic_steps.keys(),
            key=lambda x: (
                int(x) if isinstance(x, str) and x.isdigit() else float("inf")
            ),
        )
    except ValueError:
        sorted_uids_for_en = sorted(all_logic_steps.keys())  # Fallback sort

    ordered_logic_list_for_en = [
        all_logic_steps[uid] for uid in sorted_uids_for_en if uid in all_logic_steps
    ]

    for i, instruction in enumerate(ordered_logic_list_for_en):
        part_uid = instruction["instruction_uid"]
        # Usar el tipo original para la lógica de inferencia
        part_type_original = (
            instruction.get("type", "").replace(SCL_SUFFIX, "").replace("_error", "")
        )

        # Inferencia solo para tipos funcionales que no tengan EN explícito
        if (
            part_type_original in functional_block_types
            and "en" not in instruction.get("inputs", {})
            and part_uid not in processed_blocks_en_inference
            and "error" not in part_type_original
        ):  # No inferir para errores

            inferred_en_source = None
            # Buscar hacia atrás en la lista ordenada
            if i > 0:
                for j in range(i - 1, -1, -1):
                    prev_instr = ordered_logic_list_for_en[j]
                    if "error" in prev_instr.get("type", ""):
                        continue  # Saltar errores previos

                    prev_uid = prev_instr["instruction_uid"]
                    prev_type_original = (
                        prev_instr.get("type", "")
                        .replace(SCL_SUFFIX, "")
                        .replace("_error", "")
                    )

                    if prev_type_original in rlo_generators:  # Fuente RLO encontrada
                        inferred_en_source = {
                            "type": "connection",
                            "source_instruction_uid": prev_uid,
                            "source_instruction_type": prev_type_original,  # Tipo original
                            "source_pin": "out",
                        }
                        break  # Detener búsqueda
                    elif (
                        prev_type_original in functional_block_types
                    ):  # Bloque funcional previo
                        # Comprobar si este bloque tiene salida ENO conectada
                        if (prev_uid, "eno") in source_connections:
                            inferred_en_source = {
                                "type": "connection",
                                "source_instruction_uid": prev_uid,
                                "source_instruction_type": prev_type_original,  # Tipo original
                                "source_pin": "eno",
                            }
                        # Si no tiene ENO conectado, el flujo RLO se detiene aquí
                        break  # Detener búsqueda
                    elif prev_type_original in [
                        "Coil",
                        "SCoil",
                        "RCoil",
                        "SdCoil",
                        "SetCoil",
                        "ResetCoil",
                    ]:
                        # Bobinas terminan el flujo RLO
                        break  # Detener búsqueda

            # Si no se encontró fuente, conectar a PowerRail
            if inferred_en_source is None:
                inferred_en_source = {"type": "powerrail"}

            # Actualizar la instrucción EN el diccionario principal
            if part_uid in all_logic_steps:
                # Asegurar que inputs exista
                if "inputs" not in all_logic_steps[part_uid]:
                    all_logic_steps[part_uid]["inputs"] = {}
                all_logic_steps[part_uid]["inputs"]["en"] = inferred_en_source
                processed_blocks_en_inference.add(part_uid)

    # 5. Lógica ENO (añadir destinos ENO si existen)
    for source_instr_uid, eno_destinations in eno_outputs.items():
        if source_instr_uid in all_logic_steps and "error" not in all_logic_steps[
            source_instr_uid
        ].get("type", ""):
            all_logic_steps[source_instr_uid]["eno_destinations"] = eno_destinations

    # 6. Ordenar y Devolver
    final_logic_list = [
        all_logic_steps[uid] for uid in sorted_uids_for_en if uid in all_logic_steps
    ]

    return {
        "id": network_id,
        "title": network_title,
        "comment": network_comment,
        "language": network_lang,  # Lenguaje original de la red
        "logic": final_logic_list,
        # No añadir 'error' aquí a menos que el parseo completo falle
    }


# --- Función de Información del Parser ---
def get_parser_info():
    """Devuelve la información para este parser."""
    # Este parser maneja LAD, FBD y GRAPH
    return {
        "language": ["LAD", "FBD", "GRAPH"],  # Lista de lenguajes soportados
        "parser_func": parse_lad_fbd_network,  # Función a llamar
    }
