#!/usr/bin/env python3
# qet_conductors_resolve_links_fix10_verdrahtungsplan.py
# (wie vorher, Bugfix: chk_kabel/chk_debug/chk_dups als global verfügbar gemacht)

import xml.etree.ElementTree as ET
from pathlib import Path
import csv, json, re
from collections import defaultdict, deque

MAX_LINK_DEPTH = 2
LABEL_BMK_RE = re.compile(r'^-?\d')
NUMERIC_LABEL_RE = re.compile(r'^\d+(\.\d+)?$')

def clean_tag(tag):
    return tag.split('}')[-1].strip() if tag else ''

def normalize_uuid(u):
    if not u:
        return ''
    return u.strip().strip('{}').lower()

def build_parent_map(root):
    parent = {}
    for p in root.iter():
        for c in p:
            parent[c] = p
    return parent

def climb_to_diagram(elem, parent_map):
    cur = elem
    while cur is not None:
        if clean_tag(cur.tag).lower() == 'diagram':
            return cur
        cur = parent_map.get(cur, None)
    return None

def find_diagram_folio(diagram_elem):
    if diagram_elem is None:
        return ''
    for key in ('folio','page','name','title'):
        if diagram_elem.attrib.get(key):
            return diagram_elem.attrib.get(key)
    for child in diagram_elem:
        if clean_tag(child.tag).lower() in ('folio','page'):
            if child.text and child.text.strip():
                return child.text.strip()
            if child.attrib.get('value'):
                return child.attrib.get('value')
    return ''

def get_exact_child_or_attr(elem, exact_names):
    lower = [n.lower() for n in exact_names]
    for child in elem:
        t = clean_tag(child.tag).lower()
        if t in lower:
            txt = (child.text or '').strip()
            if txt:
                return txt
            for a in ('value','name','label','ref','id'):
                if child.attrib.get(a):
                    return child.attrib.get(a)
            return ''
    for k,v in elem.attrib.items():
        if k.lower() in lower:
            return v
    return ''

# ---------------- Elements ----------------
def collect_elements_info(root, parent_map):
    elements_info = {}
    for el in root.findall('.//element'):
        uuid_raw = el.attrib.get('uuid') or el.attrib.get('id') or ''
        uuid = normalize_uuid(uuid_raw)
        if not uuid:
            continue
        diag = climb_to_diagram(el, parent_map)
        folio = find_diagram_folio(diag)
        links = []
        lparent = el.find('links_uuids')
        if lparent is not None:
            for lu in lparent.findall('link_uuid'):
                u = lu.attrib.get('uuid') or lu.attrib.get('id') or ''
                if u:
                    links.append(normalize_uuid(u))
        label = ''
        name = ''
        ei = el.find('elementInformations')
        if ei is not None:
            for eic in ei.findall('elementInformation'):
                key = eic.attrib.get('name','').lower()
                txt = (eic.text or '').strip()
                if key == 'label' and txt:
                    label = label or txt
                if key == 'name' and txt:
                    name = name or txt
        tval = (el.attrib.get('type') or '').lower()
        is_placeholder = False
        if 'naechste' in tval or 'vorherige' in tval or 'next' in tval or 'previous' in tval:
            is_placeholder = True
        if 'klemmen' in tval and ('naechste' in tval or 'vorherige' in tval):
            is_placeholder = True
        dt = el.find('dynamic_texts')
        if dt is not None and not label and not is_placeholder:
            for det in dt.findall('.//dynamic_elmt_text'):
                info_node = det.find('info_name')
                txt_node = det.find('text')
                info_name = (info_node.text or '').lower() if info_node is not None and info_node.text else ''
                txt = (txt_node.text or '').strip() if txt_node is not None and txt_node.text else ''
                if info_name == 'label' and txt:
                    label = label or txt
                if info_name == 'name' and txt:
                    name = name or txt
        elements_info[uuid] = {'folio': folio or '', 'links': links, 'label': label, 'name': name, 'is_placeholder': is_placeholder}
    return elements_info

def build_canonical_map(elements_info):
    canonical = {}
    visited = set()
    for u in list(elements_info.keys()):
        if u in visited:
            continue
        stack = [u]
        group = set()
        while stack:
            x = stack.pop()
            if x in group:
                continue
            group.add(x)
            visited.add(x)
            for ln in elements_info.get(x, {}).get('links', []):
                if ln and ln not in group:
                    stack.append(ln)
        if group:
            rep = sorted(group)[0]
            for member in group:
                canonical[member] = rep
    return canonical

# ---------------- Conductors ----------------
def collect_conductors(root, parent_map):
    diagram_map = { id(d): find_diagram_folio(d) for d in root.findall('.//diagram') }
    conductors = []
    i = 0
    for conductors_parent in root.findall('.//conductors'):
        for cond in conductors_parent:
            i += 1
            attrib = dict(cond.attrib)
            e1_label = get_exact_child_or_attr(cond, ['element1_label']) or attrib.get('element1_label','') or ''
            e1_linked = get_exact_child_or_attr(cond, ['element1_linked','element1_link']) or attrib.get('element1_linked','') or attrib.get('element1_link','') or ''
            e1_name = get_exact_child_or_attr(cond, ['element1_name']) or attrib.get('element1_name','') or ''
            e2_label = get_exact_child_or_attr(cond, ['element2_label']) or attrib.get('element2_label','') or ''
            e2_linked = get_exact_child_or_attr(cond, ['element2_linked','element2_link']) or attrib.get('element2_linked','') or attrib.get('element2_link','') or ''
            e2_name = get_exact_child_or_attr(cond, ['element2_name']) or attrib.get('element2_name','') or ''
            e1_final = e1_label or e1_linked or e1_name or ''
            e2_final = e2_label or e2_linked or e2_name or ''
            conductor_color = attrib.get('conductor_color','') or ''
            tension = get_exact_child_or_attr(cond, ['tension_protocol','tension','protocol']) or attrib.get('tension_protocol','') or ''
            section = get_exact_child_or_attr(cond, ['conductor_section']) or attrib.get('conductor_section','') or ''
            function = get_exact_child_or_attr(cond, ['function','funktion']) or attrib.get('function','') or ''
            term1 = get_exact_child_or_attr(cond, ['terminalname1']) or attrib.get('terminalname1','') or ''
            term2 = get_exact_child_or_attr(cond, ['terminalname2']) or attrib.get('terminalname2','') or ''
            displaytext = attrib.get('displaytext','')
            diagram_elem = climb_to_diagram(conductors_parent, parent_map)
            folio = diagram_map.get(id(diagram_elem)) if diagram_elem is not None else ''
            if not folio:
                folio = attrib.get('folio','') or attrib.get('page','') or ''
            el1 = normalize_uuid(attrib.get('element1') or attrib.get('element1id') or '')
            el2 = normalize_uuid(attrib.get('element2') or attrib.get('element2id') or '')
            conductors.append({
                'index': i,
                'el1_uuid': el1,
                'el2_uuid': el2,
                'element1_label': e1_label,
                'element1_linked': e1_linked,
                'element1_name': e1_name,
                'element1_final': e1_final,
                'element2_label': e2_label,
                'element2_linked': e2_linked,
                'element2_name': e2_name,
                'element2_final': e2_final,
                'terminalname1': term1,
                'terminalname2': term2,
                'conductor_color': conductor_color,
                'tension_protocol': tension,
                'conductor_section': section,
                'function': function,
                'displaytext': displaytext,
                'folio': folio or '',
                'raw_attrib': attrib
            })
    return conductors

def build_el_to_conductors(conductors):
    m = defaultdict(list)
    for c in conductors:
        if c['el1_uuid']:
            m[c['el1_uuid']].append(c)
        if c['el2_uuid']:
            m[c['el2_uuid']].append(c)
    return m

# ---------------- Scoring & Resolve ----------------
def score_candidate(cond, other_uuid, elements_info, source_folio, candidate_label_hint=None):
    s = 0
    if other_uuid in elements_info:
        info = elements_info[other_uuid]
        if info.get('label') and not info.get('is_placeholder') and not NUMERIC_LABEL_RE.match(info.get('label','').strip()):
            s += 1000
            if LABEL_BMK_RE.search(info.get('label','')):
                s += 300
        if info.get('name'):
            s += 200
        if info.get('folio') and source_folio and info.get('folio') != source_folio:
            s += 80
    else:
        if candidate_label_hint:
            s += 150
            if LABEL_BMK_RE.search(candidate_label_hint):
                s += 200
    if cond.get('displaytext') == '1':
        s += 80
    if cond.get('conductor_section'):
        s += 10
    if cond.get('tension_protocol'):
        s += 5
    if cond.get('terminalname1') or cond.get('terminalname2'):
        s += 6
    if cond.get('raw_attrib', {}).get('num', '') == 'Brücke':
        s = -9999
    return s

def resolve_endpoint(cond_side_uuid, elements_info, el_to_cons, cond, cond_side_tuple, source_folio, skip_conductor_index=None, max_link_depth=MAX_LINK_DEPTH):
    candidates_debug = []
    if not cond_side_uuid or cond_side_uuid not in elements_info:
        return None, None, candidates_debug

    if not elements_info[cond_side_uuid]['links'] and not elements_info[cond_side_uuid].get('is_placeholder'):
        info = elements_info[cond_side_uuid]
        chosen_label = info.get('label') or info.get('name')
        if chosen_label and NUMERIC_LABEL_RE.match(chosen_label.strip()):
            chosen_label = None
        candidates_debug.append({'other_uuid': cond_side_uuid, 'best_score': None, 'causes': [], '_decision': {'reason': 'direct_element', 'chosen_label': chosen_label}})
        return cond_side_uuid, chosen_label, candidates_debug

    visited = {cond_side_uuid}
    q = deque([(cond_side_uuid, 0)])
    found_candidates = {}
    found_causes = defaultdict(list)
    visited_vs = set()

    while q:
        node, dist = q.popleft()
        if max_link_depth is not None and dist > max_link_depth:
            continue
        visited_vs.add(node)

        for c in el_to_cons.get(node, []):
            if skip_conductor_index and c.get('index') == skip_conductor_index:
                continue
            other = c['el1_uuid'] if c['el2_uuid'] == node else c['el2_uuid']

            if c['el1_uuid'] == node:
                label_hint = c.get('element2_linked') or c.get('element2_label') or c.get('element2_name') or None
            else:
                label_hint = c.get('element1_linked') or c.get('element1_label') or c.get('element1_name') or None

            if not other or other == node:
                if label_hint:
                    sc = score_candidate(c, other, elements_info, source_folio, candidate_label_hint=label_hint)
                    found_candidates[other] = max(found_candidates.get(other, -1), sc)
                    found_causes[other].append((c, sc, label_hint))
                continue

            other_ok = False
            if other in elements_info:
                info = elements_info[other]
                if ((info.get('label') and not info.get('is_placeholder') and not NUMERIC_LABEL_RE.match(info.get('label','').strip()))
                    or info.get('name') or (not info.get('links'))):
                    other_ok = True
            if not other_ok and label_hint:
                other_ok = True

            if other_ok:
                sc = score_candidate(c, other, elements_info, source_folio, candidate_label_hint=label_hint)
                prev = found_candidates.get(other, -1)
                if sc > prev:
                    found_candidates[other] = sc
                found_causes[other].append((c, sc, label_hint))

        if found_candidates:
            break

        for linked in elements_info.get(node, {}).get('links', []):
            if linked and linked not in visited:
                visited.add(linked)
                q.append((linked, dist+1))

        for c in el_to_cons.get(node, []):
            if skip_conductor_index and c.get('index') == skip_conductor_index:
                continue
            other = c['el1_uuid'] if c['el2_uuid'] == node else c['el2_uuid']
            if other and other not in visited:
                visited.add(other)
                q.append((other, dist+1))

    for other, causes in found_causes.items():
        best = max(c[1] for c in causes)
        causes_info = []
        for c, sc, hint in causes:
            linked_hint = None
            if c.get('element1_linked'):
                linked_hint = c.get('element1_linked')
            if c.get('element2_linked'):
                linked_hint = c.get('element2_linked') or linked_hint
            causes_info.append({'conductor_raw': c.get('raw_attrib'), 'score': sc, 'label_hint': hint, 'linked_hint': linked_hint})
        candidates_debug.append({'other_uuid': other, 'best_score': best, 'causes': causes_info})

    if found_candidates:
        chosen = sorted(found_candidates.items(), key=lambda kv: (-kv[1], kv[0]))[0][0]
        chosen_score = found_candidates.get(chosen, 0)
        chosen_label = None
        if chosen and chosen in elements_info:
            info = elements_info[chosen]
            if info.get('label') and not info.get('is_placeholder') and not NUMERIC_LABEL_RE.match(info.get('label','').strip()):
                chosen_label = info.get('label')
            elif info.get('name'):
                chosen_label = info.get('name')

        linked_candidates = []
        for c, sc, hint in found_causes.get(chosen, []):
            raw = c.get('raw_attrib', {})
            for lk_key in ('element1_linked','element2_linked'):
                lk = raw.get(lk_key)
                if lk and isinstance(lk, str) and lk.strip():
                    linked_candidates.append((lk.strip(), sc))
            if hint and isinstance(hint, str) and hint.strip():
                linked_candidates.append((hint.strip(), sc-1))

        chosen_label_after_link_pref = chosen_label

        best_link_label = None
        best_link_score = None
        if linked_candidates:
            bmks = [(lk, sc) for (lk, sc) in linked_candidates if LABEL_BMK_RE.search(lk)]
            if bmks:
                best_link_label, best_link_score = sorted(bmks, key=lambda x: -x[1])[0]
            else:
                best_link_label, best_link_score = sorted(linked_candidates, key=lambda x: -x[1])[0]

            chosen_is_generic = False
            if chosen_label:
                low = chosen_label.lower()
                if any(token in low for token in ('wechsler','digitaler','klemme','blindanschluss','nächste','vorherige','folie','blind')):
                    chosen_is_generic = True

            if (not chosen_label) or (best_link_label and LABEL_BMK_RE.search(best_link_label) and best_link_score >= chosen_score - 200) or chosen_is_generic:
                chosen_label_after_link_pref = best_link_label

        if not chosen_label_after_link_pref:
            for c, sc, hint in found_causes.get(chosen, []):
                if hint and not NUMERIC_LABEL_RE.match(str(hint).strip()):
                    chosen_label_after_link_pref = hint
                    break

        decision_summary = {
            'chosen_uuid': chosen,
            'chosen_score': chosen_score,
            'chosen_label_initial': chosen_label,
            'chosen_label_final': chosen_label_after_link_pref,
            'best_link_label': best_link_label,
            'best_link_score': best_link_score
        }
        candidates_debug.append({'_decision': decision_summary})

        return chosen, chosen_label_after_link_pref, candidates_debug

    labelled_vs = [v for v in visited_vs if v in elements_info and (elements_info[v].get('label') or elements_info[v].get('name'))]
    for v in sorted(set(labelled_vs)):
        info = elements_info[v]
        if info.get('label') and not info.get('is_placeholder') and not NUMERIC_LABEL_RE.match(info.get('label','').strip()):
            candidates_debug.append({'_decision': {'chosen_uuid': v, 'reason': 'fallback_label'}})
            return v, info.get('label'), candidates_debug
        if info.get('name'):
            candidates_debug.append({'_decision': {'chosen_uuid': v, 'reason': 'fallback_name'}})
            return v, info.get('name'), candidates_debug

    candidates_debug.append({'_decision': {'chosen_uuid': None, 'reason': 'none_found'}})
    return None, None, candidates_debug

def extract_terminal_from_candidates(candidates, chosen_uuid):
    if not candidates or not chosen_uuid:
        return ''
    for entry in candidates:
        if entry.get('other_uuid') == chosen_uuid:
            causes = entry.get('causes', [])
            if not causes:
                continue
            best = sorted(causes, key=lambda c: -c.get('score', 0))[0]
            raw = best.get('conductor_raw', {}) or {}
            el1 = normalize_uuid(raw.get('element1') or '')
            el2 = normalize_uuid(raw.get('element2') or '')
            t1 = raw.get('terminalname1') or raw.get('terminal1') or ''
            t2 = raw.get('terminalname2') or raw.get('terminal2') or ''
            if el1 and el1 == normalize_uuid(chosen_uuid):
                return t1 or ''
            if el2 and el2 == normalize_uuid(chosen_uuid):
                return t2 or ''
            if t2:
                return t2
            if t1:
                return t1
    return ''

def label_from_uuid_or_conductor_fallback(chosen_uuid, chosen_label, cond_tuple):
    if chosen_label:
        return chosen_label
    lab, linked, name = cond_tuple
    if lab:
        return lab
    if linked:
        return linked
    if name:
        return name
    return ''

# ---------------- CSV writers ----------------
def write_output_csv(rows, out_path: Path):
    header = [
        'Seite','Bauteil 1','Terminalname 1','Bauteil 2','Terminalname 2',
        'Spannung / Protokoll','Aderfarbe','Aderquerschnitt','Funktion'
    ]
    with out_path.open('w', newline='', encoding='utf-8') as f:
        w = csv.DictWriter(f, fieldnames=header)
        w.writeheader()
        for r in rows:
            w.writerow({
                'Seite': r.get('folio',''),
                'Bauteil 1': r.get('element1_label',''),
                'Terminalname 1': r.get('terminalname1',''),
                'Bauteil 2': r.get('element2_label',''),
                'Terminalname 2': r.get('terminalname2',''),
                'Spannung / Protokoll': r.get('tension_protocol',''),
                'Aderfarbe': r.get('conductor_color',''),
                'Aderquerschnitt': r.get('conductor_section',''),
                'Funktion': r.get('function',''),
            })

def write_debug_csv(debug_rows, out_path: Path):
    header = ['conductor_index','side','source_uuid','chosen_uuid','chosen_label','candidates_json']
    with out_path.open('w', newline='', encoding='utf-8') as f:
        w = csv.DictWriter(f, fieldnames=header)
        w.writeheader()
        for d in debug_rows:
            w.writerow({
                'conductor_index': d.get('index'),
                'side': d.get('side'),
                'source_uuid': d.get('source_uuid',''),
                'chosen_uuid': d.get('chosen_uuid',''),
                'chosen_label': d.get('chosen_label',''),
                'candidates_json': json.dumps(d.get('candidates',[]), ensure_ascii=False)
            })

def write_duplicates_csv(dups, out_path: Path):
    header = ['original_row_index','duplicate_row_index','Seite','Bauteil1','Terminal1','Bauteil2','Terminal2','tension','color','section','function']
    with out_path.open('w', newline='', encoding='utf-8') as f:
        w = csv.DictWriter(f, fieldnames=header)
        w.writeheader()
        for d in dups:
            r = d['duplicate_row']
            w.writerow({
                'original_row_index': d['original_row_index'],
                'duplicate_row_index': d['duplicate_row_index'],
                'Seite': r.get('folio',''),
                'Bauteil1': r.get('element1_label',''),
                'Terminal1': r.get('terminalname1',''),
                'Bauteil2': r.get('element2_label',''),
                'Terminal2': r.get('terminalname2',''),
                'tension': r.get('tension_protocol',''),
                'color': r.get('conductor_color',''),
                'section': r.get('conductor_section',''),
                'function': r.get('function','')
            })

# ---------------- Signatures ----------------
def make_sorted_endpoint(a_id, a_term, b_id, b_term):
    e1 = (a_id or '').strip(), (a_term or '').strip()
    e2 = (b_id or '').strip(), (b_term or '').strip()
    return tuple(sorted([e1, e2]))

def build_signatures_for_row(r, candidates_a, candidates_b):
    sigs = set()
    def endpoint_id(label, canon, rawuuid):
        if label:
            return label.lower()
        if canon:
            return canon
        return normalize_uuid(rawwuid if 'rawwuid' in locals() else rawuuid or '')

    id1 = endpoint_id(r.get('element1_label',''), r.get('_el1_canon',''), r.get('_el1_uuid',''))
    id2 = endpoint_id(r.get('element2_label',''), r.get('_el2_canon',''), r.get('_el2_uuid',''))
    t1 = (r.get('terminalname1') or '').strip()
    t2 = (r.get('terminalname2') or '').strip()
    sigs.add(make_sorted_endpoint(id1, t1, id2, t2))
    sigs.add(make_sorted_endpoint(id1, '', id2, ''))
    sigs.add(make_sorted_endpoint(id1, t1, id2, ''))
    sigs.add(make_sorted_endpoint(id1, '', id2, t2))
    raw1 = normalize_uuid(r.get('_el1_uuid','') or '')
    raw2 = normalize_uuid(r.get('_el2_uuid','') or '')
    if raw1 or raw2:
        sigs.add(make_sorted_endpoint(raw1, t1, raw2, t2))
        sigs.add(make_sorted_endpoint(raw1, '', raw2, ''))

    def add_from_candidates(cands):
        for entry in cands:
            for cause in entry.get('causes', []):
                raw = cause.get('conductor_raw', {}) or {}
                el1 = normalize_uuid(raw.get('element1') or '')
                el2 = normalize_uuid(raw.get('element2') or '')
                tn1 = raw.get('terminalname1') or raw.get('terminal1') or ''
                tn2 = raw.get('terminalname2') or raw.get('terminal2') or ''
                if el1 or el2:
                    sigs.add(make_sorted_endpoint(el1, tn1, el2, tn2))
                    sigs.add(make_sorted_endpoint(el1, '', el2, ''))
    add_from_candidates(candidates_a)
    add_from_candidates(candidates_b)
    return sigs

# ---------------- Helpers: folio key & endpoint sort id ----------------
def folio_sort_key(folio):
    if folio is None:
        return (1, "")
    f = str(folio)
    m = re.search(r'\d+', f)
    if m:
        try:
            return (0, int(m.group(0)))
        except:
            pass
    return (1, f.lower())

def endpoint_identifier_for_sort(label, canon, rawuuid):
    if label:
        return label.lower()
    if canon:
        return canon
    return normalize_uuid(rawuuid or '')

# ---------------- Main processing ----------------
def process_file(fp: str, verdrahtungsplan=False, kabel_filter=False, write_debug=False, write_duplicates=False):
    qet_path = Path(fp)
    tree = ET.parse(qet_path); root = tree.getroot()
    parent_map = build_parent_map(root)
    elements_info = collect_elements_info(root, parent_map)
    canonical_map = build_canonical_map(elements_info)
    conductors = collect_conductors(root, parent_map)
    el_to_cons = build_el_to_conductors(conductors)

    rows = []
    debug_rows = []
    per_cond_candidates = {}

    for cond in conductors:
        if cond.get('raw_attrib', {}).get('num', '') == 'Brücke':
            continue

        if cond.get('element1_label'):
            final_a_label = cond.get('element1_label')
            chosen_a_uuid = None
            chosen_a_label = final_a_label
            candidates_a = []
        else:
            chosen_a_uuid, chosen_a_label, candidates_a = resolve_endpoint(
                cond['el1_uuid'], elements_info, el_to_cons, cond,
                (cond.get('element1_label'), cond.get('element1_linked'), cond.get('element1_name')),
                cond.get('folio',''),
                skip_conductor_index=cond['index'], max_link_depth=MAX_LINK_DEPTH)
            final_a_label = label_from_uuid_or_conductor_fallback(chosen_a_uuid, chosen_a_label,
                                                                 (cond.get('element1_label'), cond.get('element1_linked'), cond.get('element1_name')))

        if cond.get('element2_label'):
            final_b_label = cond.get('element2_label')
            chosen_b_uuid = None
            chosen_b_label = final_b_label
            candidates_b = []
        else:
            chosen_b_uuid, chosen_b_label, candidates_b = resolve_endpoint(
                cond['el2_uuid'], elements_info, el_to_cons, cond,
                (cond.get('element2_label'), cond.get('element2_linked'), cond.get('element2_name')),
                cond.get('folio',''),
                skip_conductor_index=cond['index'], max_link_depth=MAX_LINK_DEPTH)
            final_b_label = label_from_uuid_or_conductor_fallback(chosen_b_uuid, chosen_b_label,
                                                                 (cond.get('element2_label'), cond.get('element2_linked'), cond.get('element2_name')))

        term1 = cond.get('terminalname1','') or ''
        term2 = cond.get('terminalname2','') or ''
        if not term1 and chosen_a_uuid and candidates_a:
            term1 = extract_terminal_from_candidates(candidates_a, chosen_a_uuid) or term1
        if not term2 and chosen_b_uuid and candidates_b:
            term2 = extract_terminal_from_candidates(candidates_b, chosen_b_uuid) or term2

        el1_canon = canonical_map.get(cond.get('el1_uuid','')) or normalize_uuid(cond.get('el1_uuid',''))
        el2_canon = canonical_map.get(cond.get('el2_uuid','')) or normalize_uuid(cond.get('el2_uuid',''))

        sideA_folio = ''
        sideB_folio = ''
        if chosen_a_uuid and chosen_a_uuid in elements_info:
            sideA_folio = elements_info[chosen_a_uuid].get('folio','')
        if chosen_b_uuid and chosen_b_uuid in elements_info:
            sideB_folio = elements_info[chosen_b_uuid].get('folio','')
        if not sideA_folio:
            sideA_folio = cond.get('folio','')
        if not sideB_folio:
            sideB_folio = cond.get('folio','')

        row = {
            'folio': cond.get('folio',''),
            'element1_label': final_a_label or cond.get('element1_final','') or '',
            'terminalname1': term1,
            'element2_label': final_b_label or cond.get('element2_final','') or '',
            'terminalname2': term2,
            'tension_protocol': cond.get('tension_protocol',''),
            'conductor_color': cond.get('conductor_color',''),
            'conductor_section': cond.get('conductor_section',''),
            'function': cond.get('function',''),
            '_el1_uuid': cond.get('el1_uuid',''),
            '_el2_uuid': cond.get('el2_uuid',''),
            '_el1_canon': el1_canon,
            '_el2_canon': el2_canon,
            '_cond_index': cond['index'],
            '_chosen_a_uuid': chosen_a_uuid or '',
            '_chosen_b_uuid': chosen_b_uuid or '',
            '_sideA_folio': sideA_folio or '',
            '_sideB_folio': sideB_folio or ''
        }

        rows.append(row)
        per_cond_candidates[cond['index']] = (candidates_a, candidates_b)

        if write_debug:
            debug_rows.append({
                'index': cond['index'], 'side': 'A', 'source_uuid': cond.get('el1_uuid',''),
                'chosen_uuid': chosen_a_uuid or '', 'chosen_label': chosen_a_label or '',
                'candidates': candidates_a
            })
            debug_rows.append({
                'index': cond['index'], 'side': 'B', 'source_uuid': cond.get('el2_uuid',''),
                'chosen_uuid': chosen_b_uuid or '', 'chosen_label': chosen_b_label or '',
                'candidates': candidates_b
            })

    if verdrahtungsplan and kabel_filter:
        keep = []
        keep_indexes = set()
        for r in rows:
            e1 = (r.get('element1_label') or '').lower()
            e2 = (r.get('element2_label') or '').lower()
            if ('w' in e1) or ('w' in e2):
                continue
            keep.append(r)
            keep_indexes.add(r.get('_cond_index'))
        rows = keep
        per_cond_candidates = {k:v for k,v in per_cond_candidates.items() if k in keep_indexes}

    seen_signatures = {}
    filtered_rows = []
    duplicates = []
    for idx, r in enumerate(rows):
        cond_index = r.get('_cond_index')
        cands_a, cands_b = per_cond_candidates.get(cond_index, ([],[]))
        sigs = build_signatures_for_row(r, cands_a, cands_b)
        matched_original = None
        for s in sigs:
            if s in seen_signatures:
                matched_original = seen_signatures[s]
                break
        if matched_original is not None:
            duplicates.append({'original_row_index': matched_original, 'duplicate_row_index': idx, 'duplicate_row': r.copy()})
            continue
        for s in sigs:
            seen_signatures[s] = idx
        filtered_rows.append(r.copy())

    canon_filtered = []
    for r in filtered_rows:
        id1 = endpoint_identifier_for_sort(r.get('element1_label',''), r.get('_el1_canon',''), r.get('_el1_uuid',''))
        id2 = endpoint_identifier_for_sort(r.get('element2_label',''), r.get('_el2_canon',''), r.get('_el2_uuid',''))
        if id2 and (not id1 or id2 < id1):
            r['element1_label'], r['element2_label'] = r.get('element2_label',''), r.get('element1_label','')
            r['terminalname1'], r['terminalname2'] = r.get('terminalname2',''), r.get('terminalname1','')
            r['_el1_uuid'], r['_el2_uuid'] = r.get('_el2_uuid',''), r.get('_el1_uuid','')
            r['_el1_canon'], r['_el2_canon'] = r.get('_el2_canon',''), r.get('_el1_canon','')
            r['_chosen_a_uuid'], r['_chosen_b_uuid'] = r.get('_chosen_b_uuid',''), r.get('_chosen_a_uuid','')
            new_sideA_folio = r.get('_sideB_folio','') or r.get('folio','')
            r['folio'] = new_sideA_folio or r.get('folio','')
        else:
            r['folio'] = r.get('_sideA_folio','') or r.get('folio','')
        canon_filtered.append(r)

    canon_duplicates = []
    for d in duplicates:
        r = d['duplicate_row']
        id1 = endpoint_identifier_for_sort(r.get('element1_label',''), r.get('_el1_canon',''), r.get('_el1_uuid',''))
        id2 = endpoint_identifier_for_sort(r.get('element2_label',''), r.get('_el2_canon',''), r.get('_el2_uuid',''))
        if id2 and (not id1 or id2 < id1):
            r['element1_label'], r['element2_label'] = r.get('element2_label',''), r.get('element1_label','')
            r['terminalname1'], r['terminalname2'] = r.get('terminalname2',''), r.get('terminalname1','')
            r['_el1_uuid'], r['_el2_uuid'] = r.get('_el2_uuid',''), r.get('_el1_uuid','')
            r['_el1_canon'], r['_el2_canon'] = r.get('_el2_canon',''), r.get('_el1_canon','')
            r['_chosen_a_uuid'], r['_chosen_b_uuid'] = r.get('_chosen_b_uuid',''), r.get('_chosen_a_uuid','')
            new_sideA_folio = r.get('_sideB_folio','') or r.get('folio','')
            r['folio'] = new_sideA_folio or r.get('folio','')
        else:
            r['folio'] = r.get('_sideA_folio','') or r.get('folio','')
        out_r = {k:v for k,v in r.items() if not k.startswith('_')}
        canon_duplicates.append({'original_row_index': d['original_row_index'], 'duplicate_row_index': d['duplicate_row_index'], 'duplicate_row': out_r})

    final_rows = []
    for r in canon_filtered:
        out_row = {k:v for k,v in r.items() if not k.startswith('_')}
        final_rows.append(out_row)

    def sort_key(rr):
        return (
            folio_sort_key(rr.get('folio','')),
            (rr.get('element1_label') or '').lower(),
            (rr.get('element2_label') or '').lower(),
            (rr.get('terminalname1') or ''),
            (rr.get('terminalname2') or '')
        )

    final_rows_sorted = sorted(final_rows, key=sort_key)

    if verdrahtungsplan:
        out = qet_path.with_suffix('.Verdrahtungsplan.csv')
        dbg = qet_path.with_suffix('.Verdrahtungsplan.debug.csv') if write_debug else None
        dupf = qet_path.with_suffix('.Verdrahtungsplan.duplicates.csv') if write_duplicates else None
    else:
        out = qet_path.with_suffix('.conductors.merged.csv')
        dbg = qet_path.with_suffix('.conductors.merged.debug.csv') if write_debug else None
        dupf = qet_path.with_suffix('.conductors.merged.duplicates.csv') if write_duplicates else None

    write_output_csv(final_rows_sorted, out)
    if dbg:
        write_debug_csv(debug_rows, dbg)
    if dupf:
        write_duplicates_csv(canon_duplicates, dupf)

    return out, dbg, dupf

# ---------------- CLI wrapper (statt GUI) ----------------
if __name__ == '__main__':
    import argparse
    import sys
    parser = argparse.ArgumentParser(description="Erzeuge Verdrahtungsplan / conductors CSV aus .qet Datei.")
    parser.add_argument('qet_file', help=".qet Datei (oder .xml)")
    parser.add_argument('--verd', '--verdrahtungsplan', dest='verd', action='store_true', help="Verdrahtungsplan erstellen (aktiviert zusätzliche Optionen)")
    parser.add_argument('--kabel', action='store_true', help="Kabel rausfiltern (nur wenn --verd gesetzt)")
    parser.add_argument('--debug', action='store_true', help="Debug-CSV erstellen (nur wenn --verd gesetzt)")
    parser.add_argument('--dups', action='store_true', help="Duplicates-CSV erstellen (nur wenn --verd gesetzt)")
    args = parser.parse_args()

    try:
        out, dbg, dupf = process_file(
            args.qet_file,
            verdrahtungsplan=bool(args.verd),
            kabel_filter=bool(args.kabel) and bool(args.verd),
            write_debug=bool(args.debug) and bool(args.verd),
            write_duplicates=bool(args.dups) and bool(args.verd)
        )
        print(f"Erstellt: {out}")
        if dbg:
            print(f"Debug: {dbg}")
        if dupf:
            print(f"Duplicates: {dupf}")
    except Exception as e:
        print(f"Fehler bei Verarbeitung: {e}", file=sys.stderr)
        sys.exit(1)
