#!/usr/bin/env python3
import sys
import xml.etree.ElementTree as ET
import re
import os
import csv
import io
from collections import defaultdict

def remove_namespaces(xml_string):
    try:
        it = ET.iterparse(io.StringIO(xml_string))
        for _, el in it:
            if isinstance(el.tag, str) and '}' in el.tag:
                el.tag = el.tag.split('}', 1)[1]
            for attr in list(el.attrib):
                if '}' in attr:
                    new_attr = attr.split('}', 1)[1]
                    el.attrib[new_attr] = el.attrib.pop(attr)
        return ET.tostring(it.root, encoding='unicode')
    except Exception:
        s = re.sub(r'\sxmlns(:\w+)?="[^"]+"', "", xml_string)
        s = re.sub(r'([a-zA-Z0-9_-]+):', r'', s)
        return s

def safe_int(value):
    try:
        return int(float(value))
    except:
        return 0

def bmk_prefix(bmk):
    if ":" in bmk:
        return bmk.split(":")[0]
    return bmk

def normalize_uuid(uuid_str):
    if uuid_str:
        return uuid_str.strip("{}").lower()
    return ""

def is_cable(element_data):
    if element_data.get("prefix") == "W":
        return True
    label = element_data.get("label", "").upper()
    if label.startswith("-W") or label.startswith("W"):
        return True
    type_str = element_data.get("type", "").lower()
    if "kabel" in type_str or "cable" in type_str:
        return True
    return False

def is_terminal(elem_xml, element_data):
    # Check 1: QET Terminal-Eigenschaft
    prop = elem_xml.find("properties/element_type")
    if prop is not None and prop.get("terminal_type"):
        return True
    # Check 2: Label-Konvention
    prefix = element_data.get("prefix", "")
    label = element_data.get("label", "")
    if prefix == "X" or label.startswith("-X"):
        if not is_cable(element_data):
            return True
    return False

def process_file(input_file, output_file):
    if not output_file.lower().endswith(".csv"):
        output_file = os.path.splitext(output_file)[0] + ".csv"

    try:
        with open(input_file, "r", encoding="utf-8") as f:
            xml_data = f.read()
        xml_data = remove_namespaces(xml_data)
        root = ET.fromstring(xml_data)
    except Exception as e:
        msg = f"Fehler beim Parsen der Datei '{input_file}': {e}"
        print(msg, file=sys.stderr)
        raise RuntimeError(msg)

    # --- SCHRITT 1: Elemente erfassen ---
    elements_by_uuid = {}
    raw_elements_xml = {}

    for diagram in root.findall(".//diagram"):
        folio = diagram.get("folio", "").strip()
        order_val = safe_int(diagram.get("order", "0"))

        for elem in diagram.findall(".//element"):
            uuid_norm = normalize_uuid(elem.get("uuid", ""))
            if not uuid_norm: continue

            label = (elem.get("label") or elem.get("name") or "").strip()
            unity = elem.get("unity", "").strip()
            m_ref = ""
            supp = ""

            infos = elem.find("elementInformations")
            if infos is not None:
                for info in infos.findall("elementInformation"):
                    name = info.get("name", "").lower()
                    text_val = (info.text or "").strip()
                    if name == "label": label = text_val
                    elif name == "unity": unity = text_val
                    elif name == "manufacturer_reference": m_ref = text_val
                    elif name == "supplier": supp = text_val

            data = {
                "uuid": uuid_norm,
                "type": elem.get("type", ""),
                "prefix": elem.get("prefix", ""),
                "label": label,
                "unity": unity,
                "x": safe_int(elem.get("x", "0")),
                "y": safe_int(elem.get("y", "0")),
                "folio": folio,
                "order": order_val,
                "manufacturer_reference": m_ref,
                "supplier": supp
            }
            elements_by_uuid[uuid_norm] = data
            raw_elements_xml[uuid_norm] = elem

    # --- SCHRITT 2: Verbindungs-Graph & Globale Leiterliste ---
    adjacency = defaultdict(list)
    all_conductors = []
    for conductor in root.findall(".//conductor"):
        e1 = normalize_uuid(conductor.get("element1", ""))
        e2 = normalize_uuid(conductor.get("element2", ""))
        # read terminal UUIDs and terminalname attributes if present
        t1 = normalize_uuid(conductor.get("terminal1", "")) if conductor.get("terminal1", "") else ""
        t2 = normalize_uuid(conductor.get("terminal2", "")) if conductor.get("terminal2", "") else ""
        tname1 = (conductor.get("terminalname1") or "").strip()
        tname2 = (conductor.get("terminalname2") or "").strip()
        all_conductors.append({
            'e1': e1, 'e2': e2, 't1': t1, 't2': t2,
            'tname1': tname1, 'tname2': tname2,
            'xml': conductor
        })
        if e1 in elements_by_uuid and e2 in elements_by_uuid:
            adjacency[e1].append((e2, conductor))
            adjacency[e2].append((e1, conductor))

    # index conductors by element for fast lookup
    cond_index = defaultdict(list)
    for c in all_conductors:
        cond_index[c['e1']].append(c)
        cond_index[c['e2']].append(c)

    # --- SCHRITT 3: Klemmen filtern & sortieren (STAND: FUNKTIONIERT) ---
    klemmen_items = []
    for uid, data in elements_by_uuid.items():
        elem_xml = raw_elements_xml.get(uid)
        prop = None
        if elem_xml is not None:
            prop = elem_xml.find("properties/element_type")
        if prop is None or not prop.get("terminal_type"):
            continue
        if data["label"]:
            klemmen_items.append(data)

    klemmen_items.sort(key=lambda i: (
        bmk_prefix(i["label"]).lower(),
        i["order"],
        i["x"],
        i["y"]
    ))

    # --- SCHRITT 4: CSV generieren mit Ader-Übereinstimmung per Terminalname (kein Fallback) ---
    header = ["Seitennummer", "ziel extern", "Kabel", "Kabeltyp", "Klemme"] + ["Brücke"] * 10 + ["Klemmentyp", "Ebene"]
    output_rows = []
    prev_prefix = None
    func_map = {}

    for item in klemmen_items:
        uid = item["uuid"]
        bmk = item["label"]
        current_prefix = bmk_prefix(bmk)

        if prev_prefix and current_prefix != prev_prefix:
            func_map = {}
            output_rows.append([""] * len(header))
        prev_prefix = current_prefix

        # Vorbereitung
        ziel_extern = ""
        kabel_labels = set()
        kabel_typen = set()
        bruecke = [""] * 10

        # Brücken über adjacency prüfen (unverändert)
        if uid in adjacency:
            for neighbor_uuid, cond_xml in adjacency[uid]:
                fn = (cond_xml.get("tension_protocol", "") or "").strip().upper()
                if fn:
                    if fn not in func_map and len(func_map) < 10:
                        func_map[fn] = len(func_map) + 1
                    idx = func_map.get(fn)
                    if idx and 1 <= idx <= 10:
                        bruecke[idx - 1] = "•"

        # Für jede Leitung, die an die aktuelle Klemme hängt
        for c in cond_index.get(uid, []):
            # bestimme Nachbar
            neighbor_uuid = c['e2'] if c['e1'] == uid else c['e1']
            if not neighbor_uuid:
                continue
            neighbor = elements_by_uuid.get(neighbor_uuid)
            if not neighbor:
                continue

            # Falls Nachbar ein Kabel ist: Ader-Matching per terminalname
            if is_cable(neighbor):
                kabel_labels.add(neighbor.get("label",""))
                if neighbor.get("unity"):
                    kabel_typen.add(neighbor.get("unity"))

                cable_uuid = neighbor['uuid']
                cable_y = neighbor.get("y", 0)

                # Bestimme terminalname auf Kabel-Seite für diese Verbindung
                if c['e1'] == cable_uuid:
                    cable_termname = (c.get('tname1') or "").strip()
                else:
                    cable_termname = (c.get('tname2') or "").strip()

                # Wenn kein terminalname vorhanden: kein Eintrag (kein Fallback)
                if not cable_termname:
                    continue

                cable_termname_norm = cable_termname.strip().lower()

                # Suche auf *diesem Kabel* alle conductors mit exakt demselben kabel-seitigen terminalname
                matches = []
                for c2 in cond_index.get(cable_uuid, []):
                    # skip same conductor
                    if c2 is c:
                        continue
                    # ermittle kabelseitige terminalname von c2
                    if c2['e1'] == cable_uuid:
                        tname_c2 = (c2.get('tname1') or "").strip()
                        other_uuid = c2['e2']
                    else:
                        tname_c2 = (c2.get('tname2') or "").strip()
                        other_uuid = c2['e1']
                    if not tname_c2:
                        continue
                    if tname_c2.strip().lower() != cable_termname_norm:
                        continue
                    # other element
                    if not other_uuid or other_uuid == uid:
                        continue
                    target_el = elements_by_uuid.get(other_uuid)
                    if not target_el:
                        continue
                    # ignore if target is a cable
                    if is_cable(target_el):
                        continue
                    # exclude same BMK-prefix (schrankintern)
                    if bmk_prefix(target_el.get("label","")) == current_prefix:
                        continue
                    matches.append(target_el)

                # Nur wenn genau ein eindeutiger Treffer vorhanden ist, wird das Ziel gesetzt
                if len(matches) == 1:
                    ziel_extern = matches[0].get("label", "")
                    # Ziel eindeutig gefunden für diese Ader -> wir können abbrechen
                    break
                else:
                    # 0 oder >1 Treffer -> kein Eintrag (kein Fallback)
                    continue

            else:
                # Direkte Verbindung ohne Kabel: akzeptiere, wenn nicht schrankintern und geometrisch darunter
                if bmk_prefix(neighbor.get("label","")) != current_prefix:
                    if neighbor.get("y", 0) > item.get("y", 0):
                        ziel_extern = neighbor.get("label","")
                        break

        # Zeile zusammenbauen
        row = [
            item.get("folio",""),
            ziel_extern,
            ", ".join(sorted(kabel_labels)),
            ", ".join(sorted(kabel_typen)),
            bmk
        ] + bruecke + [item.get("manufacturer_reference",""), item.get("supplier","")]

        output_rows.append(row)

    # --- SCHRITT 5: Schreiben ---
    try:
        with open(output_file, "w", encoding="utf-8-sig", newline="") as csvfile:
            writer = csv.writer(csvfile, delimiter=";")
            writer.writerow(header)
            writer.writerows(output_rows)
        print(f"Erfolgreich gespeichert in '{output_file}'.")
    except Exception as e:
        print("Fehler beim Schreiben:", e)

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("Verwendung: python3 Skript.py <Input-XML> <Output-CSV>")
        sys.exit(1)
    process_file(sys.argv[1], sys.argv[2])
