"""
Setzt Koordinatensystem-Bloecke (K1, K2, ...) in Omniflo DXF-Dateien.

Jeder Block enthaelt drei Linien vom Ursprung:
  - rot(1)   X-Achse, Laenge 1
  - gruen(3) Y-Achse, Laenge 2
  - blau(5)  Z-Achse, Laenge 3

Schalter:
  --k1set          K1 an alle Boegen und Weichen setzen.
                   Boegen:  Beginn der obersten horizontalen Linie,
                            x rechts, y oben, z aus Zeichenebene.
                   Weichen: Beginn der untersten vertikalen Linie,
                            x oben, y links, z aus Zeichenebene.
  --k2set          K2 an alle Boegen und Weichen setzen.
                   Nutzt dieselben Schalter-Filter wie set_einfuegepkt.py
                   (SWITCH_FILTERS aus utils.py).

                   Boegen:
                     Position am anderen Ende der Kette (Gerade-Bogen-Gerade).
                     x-Achse tangential zur Linie, zur Bogenmitte zeigend.
                     y senkrecht dazu, z wie K1 (aus Zeichenebene).
                     180-Grad-Sonderfall: x nach rechts (rz=0).

                   Weichen (weichen45, weichen90, weichen_parallel):
                     Folgt dem Bogenzweig bis zum Ende.
                     x tangential zur Linie, zur Kurve zeigend.
                     Bei Doppelweichen: linker Zweig.
                     y senkrecht dazu, z wie K1.

  --k3set          K3 an Weichen mit 2+ Boegen setzen (rechter Zweig).
                   Gleiche Logik wie K2, aber rechter Ast (groesstes x).
                   Nur bei: Doppelweichen, Dreiwegeweichen,
                   Dreifachweiche, Delta, Stern, Weichenkoerper Doppel/Dreiwege.
  --k4set          K4 an Weichen setzen (oberes Ende der K1-Linie).
                   K4 liegt K1 gegenueber, am oberen Ende derselben V-Linie.
                   Rotation wie K1: x oben, y links, z aus Zeichenebene.
                   Kein K4 bei: Boegen, Doppelweiche, Dreifachweiche, Delta.
  --set-all        Alle Koordinatensysteme (K1-K4) in einem Durchgang setzen.
  --force          Bestehende Kx ueberschreiben (sonst skip wenn vorhanden).
                   Duplikate werden immer bereinigt (letztes bleibt).
  --show-omniflo   Uebersichts-DXF mit K-Positionen als farbige Kreuze.
  --test           Erzeugt Testdatei mit 4 KOS und verifiziert diese.
  --number SIVA    Nur diese eine 9-stellige Sivasnr verarbeiten.
"""

import argparse
import json
import math
import os
import sys

import ezdxf
from ezdxf.math import Matrix44

from utils import (
    ROW_GROUPS, TEXT_HEIGHT, TEXT_MARGIN, CROSS_SIZE, ROW_LABEL_WIDTH,
    SWITCH_FILTERS,
    load_omniflo_data, build_row_layout, import_element_as_block,
    draw_cross, is_bogen,
)


# ---------------------------------------------------------------------------
# Block-Definition
# ---------------------------------------------------------------------------

KS_LINES = [
    (1, 1.0, 0.0, 0.0),   # rot, X-Achse, Laenge 1
    (3, 0.0, 2.0, 0.0),   # gruen, Y-Achse, Laenge 2
    (5, 0.0, 0.0, 3.0),   # blau, Z-Achse, Laenge 3
]


def _ensure_block(doc, block_name):
    """Erzeugt den Block falls er noch nicht existiert."""
    if block_name not in doc.blocks:
        blk = doc.blocks.new(name=block_name)
        for color, dx, dy, dz in KS_LINES:
            blk.add_line(
                start=(0, 0, 0),
                end=(dx, dy, dz),
                dxfattribs={"color": color},
            )
    return block_name


def _ensure_layer(doc, layer_name="_KOORDINATENSYSTEME"):
    """Stellt sicher, dass der Layer existiert."""
    if layer_name not in doc.layers:
        doc.layers.add(layer_name)


# ---------------------------------------------------------------------------
# insert / read / verify
# ---------------------------------------------------------------------------

def insert_ks(msp, name, point, rx=0, ry=0, rz=0):
    """
    Erzeugt ein benanntes Koordinatensystem als Block-Referenz.

    Args:
        msp: Modelspace oder Block des Ziel-Dokuments.
        name: Block-Name (z.B. "K1", "K2", "K3", "K4").
        point: Einfuegepunkt als (x, y, z) Tupel.
        rx: Rotation um X-Achse in Grad.
        ry: Rotation um Y-Achse in Grad.
        rz: Rotation um Z-Achse in Grad.

    Returns:
        Das erzeugte INSERT-Entity.
    """
    doc = msp.doc
    _ensure_layer(doc)
    _ensure_block(doc, name)

    insert = msp.add_blockref(
        name,
        insert=point,
        dxfattribs={"layer": "_KOORDINATENSYSTEME"},
    )

    m = Matrix44.chain(
        Matrix44.translate(-point[0], -point[1], -point[2]),
        Matrix44.x_rotate(math.radians(rx)),
        Matrix44.y_rotate(math.radians(ry)),
        Matrix44.z_rotate(math.radians(rz)),
        Matrix44.translate(point[0], point[1], point[2]),
    )
    insert.transform(m)

    return insert


def read_ks(doc, name):
    """
    Liest ein Koordinatensystem aus einer DXF-Datei zurueck.

    Returns:
        Dict mit 'name', 'point', 'rx', 'ry', 'rz' oder None.
    """
    msp = doc.modelspace()
    for entity in msp:
        if entity.dxftype() == "INSERT" and entity.dxf.name == name:
            lines = [e for e in entity.virtual_entities() if e.dxftype() == "LINE"]
            if len(lines) != 3:
                return None

            point = (lines[0].dxf.start[0], lines[0].dxf.start[1], lines[0].dxf.start[2])

            axes = {}
            for line in lines:
                end = line.dxf.end
                dx = end[0] - point[0]
                dy = end[1] - point[1]
                dz = end[2] - point[2]
                length = (dx**2 + dy**2 + dz**2) ** 0.5
                axes[line.dxf.color] = (dx / length, dy / length, dz / length)

            x_axis = axes.get(1, (1, 0, 0))
            y_axis = axes.get(3, (0, 1, 0))
            z_axis = axes.get(5, (0, 0, 1))

            ry = math.asin(max(-1, min(1, -x_axis[2])))
            cos_ry = math.cos(ry)

            if abs(cos_ry) > 1e-6:
                rx = math.atan2(y_axis[2] / cos_ry, z_axis[2] / cos_ry)
                rz = math.atan2(x_axis[1] / cos_ry, x_axis[0] / cos_ry)
            else:
                rz = 0
                rx = math.atan2(-z_axis[0], y_axis[0])

            return {
                "name": name,
                "point": point,
                "rx": math.degrees(rx),
                "ry": math.degrees(ry),
                "rz": math.degrees(rz),
            }
    return None


def verify_ks(dxf_path, expected):
    """Liest KS-Bloecke und vergleicht mit Erwartungswerten (Toleranz 0.01)."""
    doc = ezdxf.readfile(dxf_path)
    all_ok = True
    tol = 0.01

    for exp in expected:
        result = read_ks(doc, exp["name"])
        if result is None:
            print(f"FEHLER: Block '{exp['name']}' nicht gefunden")
            all_ok = False
            continue

        errors = []
        for i, axis in enumerate(("x", "y", "z")):
            if abs(result["point"][i] - exp["point"][i]) > tol:
                errors.append(f"{axis}={result['point'][i]:.3f} (erwartet {exp['point'][i]:.3f})")

        for angle in ("rx", "ry", "rz"):
            diff = abs(result[angle] - exp[angle])
            if diff > 360 - tol:
                diff = abs(diff - 360)
            if diff > tol:
                errors.append(f"{angle}={result[angle]:.2f} (erwartet {exp[angle]:.2f})")

        if errors:
            print(f"FEHLER {exp['name']}: {', '.join(errors)}")
            all_ok = False
        else:
            print(f"OK     {exp['name']}: point=({result['point'][0]:.2f},{result['point'][1]:.2f},{result['point'][2]:.2f}) "
                  f"rx={result['rx']:.2f} ry={result['ry']:.2f} rz={result['rz']:.2f}")

    return all_ok


# ---------------------------------------------------------------------------
# K1-Positionsbestimmung
# ---------------------------------------------------------------------------

def find_topmost_horizontal_line(doc):
    """Findet die am weitesten oben liegende horizontale Linie."""
    best = None
    best_y = -float('inf')
    for entity in doc.modelspace():
        if entity.dxftype() != "LINE":
            continue
        s, e = entity.dxf.start, entity.dxf.end
        if abs(e[1] - s[1]) > 0.01:
            continue
        if s[1] > best_y:
            best_y = s[1]
            best = entity
    return best


def find_bottommost_vertical_line(doc):
    """Findet die am weitesten unten liegende rein vertikale Linie."""
    best = None
    best_min_y = float('inf')
    for entity in doc.modelspace():
        if entity.dxftype() != "LINE":
            continue
        s, e = entity.dxf.start, entity.dxf.end
        if abs(e[0] - s[0]) > 0.01:
            continue
        min_y = min(s[1], e[1])
        if min_y < best_min_y:
            best_min_y = min_y
            best = entity
    return best


def k1_point_bogen(doc):
    """K1-Position fuer Boegen: Beginn der obersten horizontalen Linie.
    Rotation: x rechts, y oben, z aus Zeichenebene (Standard, rz=0).
    """
    hline = find_topmost_horizontal_line(doc)
    if hline is None:
        return None
    s, e = hline.dxf.start, hline.dxf.end
    left_x = min(s[0], e[0])
    y = s[1]
    return (left_x, y, 0), 0, 0, 0


def k1_point_weiche(doc):
    """K1-Position fuer Weichen: Beginn der untersten vertikalen Linie.
    Rotation: x oben, y links, z aus Zeichenebene (rz=90).
    """
    vline = find_bottommost_vertical_line(doc)
    if vline is None:
        return None
    s, e = vline.dxf.start, vline.dxf.end
    x = s[0]
    bottom_y = min(s[1], e[1])
    return (x, bottom_y, 0), 0, 0, 90


# ---------------------------------------------------------------------------
# K2-Positionsbestimmung (nur Boegen)
# ---------------------------------------------------------------------------

def k4_point_weiche(doc):
    """K4-Position fuer Weichen: oberes Ende der K1-Vertikallinie.

    K4 liegt K1 gegenueber, am oberen Ende derselben vertikalen Linie.
    Rotation wie K1: x oben, y links, z aus Zeichenebene (rz=90).
    """
    vline = find_bottommost_vertical_line(doc)
    if vline is None:
        return None
    s, e = vline.dxf.start, vline.dxf.end
    x = s[0]
    top_y = max(s[1], e[1])
    return (x, top_y, 0), 0, 0, 90


# ---------------------------------------------------------------------------
# K2-Positionsbestimmung (nur Boegen)
# ---------------------------------------------------------------------------

def k2_point_bogen(doc):
    """K2-Position fuer Boegen: anderes Ende der Kette (Gerade-Bogen-Gerade).

    Sucht den Bogen, bestimmt welches Ende zur K1-Seite (oberste H-Linie)
    gehoert, findet die laengste Linie am anderen Bogenende und setzt K2
    an deren aeusseres Ende.

    x-Achse tangential zur Linie (Richtung weg vom Bogen),
    y senkrecht dazu, z aus Zeichenebene (wie K1).
    Sonderfall 180-Grad-Bogen: x nach rechts (rz=0).
    """
    msp = doc.modelspace()

    # Bogen finden
    arc = None
    for e in msp:
        if e.dxftype() == "ARC":
            arc = e
            break
    if arc is None:
        return None

    cx, cy = arc.dxf.center[0], arc.dxf.center[1]
    r = arc.dxf.radius
    sa, ea = arc.dxf.start_angle, arc.dxf.end_angle

    arc_start = (cx + r * math.cos(math.radians(sa)),
                 cy + r * math.sin(math.radians(sa)))
    arc_end = (cx + r * math.cos(math.radians(ea)),
               cy + r * math.sin(math.radians(ea)))

    # K1-Seite = oberste horizontale Linie
    hline = find_topmost_horizontal_line(doc)
    if hline is None:
        return None
    h_y = hline.dxf.start[1]

    # Welches Bogenende liegt an der K1-H-Linie?
    tol = 1.0
    if abs(arc_end[1] - h_y) < tol:
        k2_junction = arc_start
    else:
        k2_junction = arc_end

    # Laengste Linie am K2-Bogenende suchen
    best_line_len = 0
    k2_pos = None
    junc_pt = None
    for e in msp:
        if e.dxftype() != "LINE":
            continue
        s = (e.dxf.start[0], e.dxf.start[1])
        end = (e.dxf.end[0], e.dxf.end[1])
        ds = ((s[0] - k2_junction[0])**2 + (s[1] - k2_junction[1])**2) ** 0.5
        de = ((end[0] - k2_junction[0])**2 + (end[1] - k2_junction[1])**2) ** 0.5
        if ds > tol and de > tol:
            continue
        ln = ((end[0] - s[0])**2 + (end[1] - s[1])**2) ** 0.5
        if ln > best_line_len:
            best_line_len = ln
            if ds < tol:
                k2_pos = end
                junc_pt = s
            else:
                k2_pos = s
                junc_pt = end

    if k2_pos is None:
        return None

    # Bogenspanne berechnen
    span = (ea - sa) % 360

    # Tangenten-Richtung: von K2 zur Junction (zur Bogenmitte)
    dx = junc_pt[0] - k2_pos[0]
    dy = junc_pt[1] - k2_pos[1]
    rz = math.degrees(math.atan2(dy, dx))

    # 180-Grad-Sonderfall: x nach rechts, wie K1
    if abs(span - 180) < 1:
        rz = 0

    return (k2_pos[0], k2_pos[1], 0), 0, 0, rz


# ---------------------------------------------------------------------------
# K2-Positionsbestimmung Weichen
# ---------------------------------------------------------------------------

def _find_weiche_arc_branches(doc):
    """Findet alle Bogenzweig-Endpunkte einer Weiche.

    Returns:
        Liste von (k2_pos, junc_pt) Tupeln, sortiert nach x (links zuerst),
        oder None falls keine Boegen gefunden.
    """
    msp = doc.modelspace()

    # K1-Position bestimmen (unterste vertikale Linie)
    vline = find_bottommost_vertical_line(doc)
    if vline is None:
        return None
    s, e = vline.dxf.start, vline.dxf.end
    k1_x = s[0]
    k1_y = min(s[1], e[1])

    # Alle Boegen sammeln
    arcs = [e for e in msp if e.dxftype() == "ARC"]
    if not arcs:
        return None

    tol = 1.0

    # Fuer jeden Bogen: K2-seitiges Ende bestimmen und Endpunkt der Linie finden
    candidates = []
    for arc in arcs:
        cx, cy = arc.dxf.center[0], arc.dxf.center[1]
        r = arc.dxf.radius
        sa, ea = arc.dxf.start_angle, arc.dxf.end_angle

        arc_start = (cx + r * math.cos(math.radians(sa)),
                     cy + r * math.sin(math.radians(sa)))
        arc_end = (cx + r * math.cos(math.radians(ea)),
                   cy + r * math.sin(math.radians(ea)))

        # K1-Seite = naeher an K1
        d_start = ((arc_start[0] - k1_x)**2 + (arc_start[1] - k1_y)**2) ** 0.5
        d_end = ((arc_end[0] - k1_x)**2 + (arc_end[1] - k1_y)**2) ** 0.5

        if d_start < d_end:
            k2_junction = arc_end
        else:
            k2_junction = arc_start

        # Laengste Linie am K2-Bogenende suchen
        best_len = 0
        k2_pos = None
        junc_pt = None
        for ent in msp:
            if ent.dxftype() != "LINE":
                continue
            ls = (ent.dxf.start[0], ent.dxf.start[1])
            le = (ent.dxf.end[0], ent.dxf.end[1])
            ds = ((ls[0] - k2_junction[0])**2 + (ls[1] - k2_junction[1])**2) ** 0.5
            de = ((le[0] - k2_junction[0])**2 + (le[1] - k2_junction[1])**2) ** 0.5
            if ds > tol and de > tol:
                continue
            ln = ((le[0] - ls[0])**2 + (le[1] - ls[1])**2) ** 0.5
            if ln > best_len:
                best_len = ln
                if ds < tol:
                    k2_pos = le
                    junc_pt = ls
                else:
                    k2_pos = ls
                    junc_pt = le

        if k2_pos is not None:
            candidates.append((k2_pos, junc_pt))

    if not candidates:
        return None

    # Sortieren nach x: links (kleinstes x) zuerst, rechts (groesstes x) zuletzt
    candidates.sort(key=lambda c: c[0][0])
    return candidates


def _pick_branch(candidates, branch):
    """Waehlt linken oder rechten Zweig aus und berechnet rz."""
    if branch == "left":
        k2_pos, junc_pt = candidates[0]
    else:
        k2_pos, junc_pt = candidates[-1]

    # x-Achse tangential zur Linie, zur Kurve zeigend
    dx = junc_pt[0] - k2_pos[0]
    dy = junc_pt[1] - k2_pos[1]
    rz = math.degrees(math.atan2(dy, dx))

    return (k2_pos[0], k2_pos[1], 0), 0, 0, rz


def k2_point_weiche_arc(doc):
    """K2 fuer Weichen: Ende des linken Bogenzweigs."""
    candidates = _find_weiche_arc_branches(doc)
    if candidates is None:
        return None
    return _pick_branch(candidates, "left")


def k3_point_weiche_arc(doc):
    """K3 fuer Weichen: Ende des rechten Bogenzweigs.

    Nur bei Weichen mit 2+ Boegen (Doppel-, Dreiwegeweichen etc.).
    Bei Einzelweichen (nur 1 Bogen) gibt es keinen rechten Zweig.
    """
    candidates = _find_weiche_arc_branches(doc)
    if candidates is None or len(candidates) < 2:
        return None
    return _pick_branch(candidates, "right")


# ---------------------------------------------------------------------------
# K2/K3 fuer Weichenkoerper (keine ARCs, nur Diagonalen)
# ---------------------------------------------------------------------------

def _find_weichenkoerper_diag_branches(doc):
    """Findet die Enden der langen Diagonalen eines Weichenkoerpers.

    Weichenkoerper haben keine Boegen, sondern Diagonalen die sich an einem
    Punkt nahe der K1-Vertikallinie treffen. K2/K3 liegen an den aeusseren
    Enden der langen Diagonalen (Laenge > 100).

    Returns:
        Liste von (outer_pt, junction_pt) Tupeln, sortiert nach x (links zuerst),
        oder None falls keine langen Diagonalen gefunden.
    """
    msp = doc.modelspace()

    # K1-Position bestimmen (unterste vertikale Linie)
    vline = find_bottommost_vertical_line(doc)
    if vline is None:
        return None
    s, e = vline.dxf.start, vline.dxf.end
    k1_x = s[0]
    k1_y = min(s[1], e[1])

    # Lange Diagonalen finden (nicht horizontal, nicht vertikal, Laenge > 100)
    candidates = []
    for ent in msp:
        if ent.dxftype() != "LINE":
            continue
        ls = (ent.dxf.start[0], ent.dxf.start[1])
        le = (ent.dxf.end[0], ent.dxf.end[1])
        dx = abs(le[0] - ls[0])
        dy = abs(le[1] - ls[1])
        if dx < 0.01 or dy < 0.01:
            continue
        ln = (dx**2 + dy**2) ** 0.5
        if ln < 100:
            continue

        # Das Ende naeher an K1 ist die Junction, das andere ist der K2/K3-Punkt
        d_start = ((ls[0] - k1_x)**2 + (ls[1] - k1_y)**2) ** 0.5
        d_end = ((le[0] - k1_x)**2 + (le[1] - k1_y)**2) ** 0.5
        if d_start < d_end:
            candidates.append((le, ls))
        else:
            candidates.append((ls, le))

    if not candidates:
        return None

    # Sortieren nach x: links (kleinstes x) zuerst
    candidates.sort(key=lambda c: c[0][0])
    return candidates


def k2_point_weichenkoerper(doc):
    """K2 fuer Weichenkoerper: Ende der linken langen Diagonale."""
    candidates = _find_weichenkoerper_diag_branches(doc)
    if candidates is None:
        return None
    outer_pt, junc_pt = candidates[0]
    dx = junc_pt[0] - outer_pt[0]
    dy = junc_pt[1] - outer_pt[1]
    rz = math.degrees(math.atan2(dy, dx))
    return (outer_pt[0], outer_pt[1], 0), 0, 0, rz


def k3_point_weichenkoerper(doc):
    """K3 fuer Weichenkoerper: Ende der rechten langen Diagonale.

    Nur bei Doppel- und Dreiwegeweichenkoerpern (2+ Diagonalen).
    """
    candidates = _find_weichenkoerper_diag_branches(doc)
    if candidates is None or len(candidates) < 2:
        return None
    outer_pt, junc_pt = candidates[-1]
    dx = junc_pt[0] - outer_pt[0]
    dy = junc_pt[1] - outer_pt[1]
    rz = math.degrees(math.atan2(dy, dx))
    return (outer_pt[0], outer_pt[1], 0), 0, 0, rz


# ---------------------------------------------------------------------------
# K2/K3 fuer Dreifach-, Delta-, Sternweiche (Enden der laengsten H-Linie)
# ---------------------------------------------------------------------------

def _find_longest_hline(doc):
    """Findet die laengste horizontale Linie im Modelspace."""
    best = None
    best_len = 0
    for e in doc.modelspace():
        if e.dxftype() != "LINE":
            continue
        s, end = e.dxf.start, e.dxf.end
        if abs(end[1] - s[1]) > 0.01:
            continue
        ln = abs(end[0] - s[0])
        if ln > best_len:
            best_len = ln
            best = e
    return best


def k2_point_hline(doc):
    """K2 am linken Ende der laengsten horizontalen Linie. x zeigt nach rechts."""
    hline = _find_longest_hline(doc)
    if hline is None:
        return None
    s, e = hline.dxf.start, hline.dxf.end
    lx = min(s[0], e[0])
    y = s[1]
    return (lx, y, 0), 0, 0, 0


def k3_point_hline(doc):
    """K3 am rechten Ende der laengsten horizontalen Linie. x zeigt nach links."""
    hline = _find_longest_hline(doc)
    if hline is None:
        return None
    s, e = hline.dxf.start, hline.dxf.end
    rx = max(s[0], e[0])
    y = s[1]
    return (rx, y, 0), 0, 0, 180


# ---------------------------------------------------------------------------
# K2/K3 Schalter-Zuordnung: SWITCH_FILTERS-Key -> Funktion
# K2 = linker Zweig, K3 = rechter Zweig (nur bei 2+ Boegen/Diagonalen)
# ---------------------------------------------------------------------------

K2_FUNCS = {
    "boegen": k2_point_bogen,
    "weichen45": k2_point_weiche_arc,
    "weichen90": k2_point_weiche_arc,
    "weichenkoerper": k2_point_weichenkoerper,
    "weichen_parallel": k2_point_weiche_arc,
    "sternweiche": k2_point_hline,
    "delta": k2_point_hline,
    "dreifachweiche": k2_point_hline,
}

K3_FUNCS = {
    "weichen45": k3_point_weiche_arc,
    "weichen90": k3_point_weiche_arc,
    "weichenkoerper": k3_point_weichenkoerper,
    "weichen_parallel": k3_point_weiche_arc,
    "sternweiche": k3_point_hline,
    "delta": k3_point_hline,
    "dreifachweiche": k3_point_hline,
}

# K4: oberes Ende der K1-Linie. Kein K4 bei: Boegen, Doppelweiche,
# Dreifachweiche, Deltaweiche (inkl. Weichenkoerper Doppel).
K4_FUNCS = {
    "weichen45": k4_point_weiche,
    "weichen90": k4_point_weiche,
    "weichenkoerper": k4_point_weiche,
    "weichen_parallel": k4_point_weiche,
    "sternweiche": k4_point_weiche,
}

_K4_EXCLUDE_TYPES = ("Doppelweiche", "Dreifachweiche", "Deltaweiche")


# ---------------------------------------------------------------------------
# Existenz-Pruefung und Duplikat-Bereinigung
# ---------------------------------------------------------------------------

def _check_and_clean_ks(msp, k_name, sivasnr, force=False):
    """Prueft ob ein Kx bereits existiert und bereinigt Duplikate.

    - Mehrfach vorhandene Kx: alle bis auf das letzte entfernen (immer).
    - Genau ein Kx vorhanden und kein --force: skip (return False).
    - Genau ein Kx vorhanden und --force: entfernen (return True).
    - Kein Kx vorhanden: return True (neu erzeugen).

    Returns:
        True  = Kx soll (neu) erzeugt werden.
        False = Kx existiert bereits, skip.
    """
    existing = [e for e in msp if e.dxftype() == "INSERT" and e.dxf.name == k_name]

    if len(existing) > 1:
        # Duplikate: alle bis auf das letzte entfernen
        for e in existing[:-1]:
            msp.delete_entity(e)
        print(f"  {sivasnr}: {len(existing)-1} Duplikat(e) von {k_name} entfernt.")
        existing = existing[-1:]

    if len(existing) == 1:
        if force:
            msp.delete_entity(existing[0])
            return True
        else:
            print(f"  {sivasnr}: skipping {k_name} (bereits vorhanden)")
            return False

    return True


# ---------------------------------------------------------------------------
# --k1set
# ---------------------------------------------------------------------------

def process_k1set(data_dir, results_dir, number=None, force=False):
    """Setzt K1-Block an alle Boegen und Weichen."""
    sources = load_omniflo_data(data_dir)
    omniflo_dir = os.path.join(data_dir, "omniflo")

    all_items = []
    for b in sources["boegen"]:
        all_items.append((str(b["Sivasnr"]), b["ProfilTyp"], True))
    for w in sources["weichen"]:
        all_items.append((str(w["Sivasnr"]), w["ProfilTyp"], False))

    if number:
        all_items = [(s, p, ib) for s, p, ib in all_items if s == str(number)]

    if not all_items:
        print("Keine Elemente gefunden.")
        return

    print("=== K1 setzen ===")
    for sivasnr, profil, is_bog in all_items:
        dxf_path = os.path.join(results_dir, f"{sivasnr}.dxf")
        if not os.path.exists(dxf_path):
            dxf_path = os.path.join(omniflo_dir, f"{sivasnr}.dxf")
        if not os.path.exists(dxf_path):
            continue

        doc = ezdxf.readfile(dxf_path)
        msp = doc.modelspace()

        if not _check_and_clean_ks(msp, "K1", sivasnr, force):
            continue

        if is_bog:
            result = k1_point_bogen(doc)
        else:
            result = k1_point_weiche(doc)

        if result is None:
            print(f"WARNUNG: K1 in {sivasnr}.dxf nicht bestimmbar, ueberspringe.")
            continue

        point, rx, ry, rz = result
        insert_ks(msp, "K1", point, rx, ry, rz)

        out_path = os.path.join(results_dir, f"{sivasnr}.dxf")
        doc.saveas(out_path)
        print(f"  {sivasnr}: K1 at ({point[0]:.2f},{point[1]:.2f},{point[2]:.2f}) "
              f"rx={rx} ry={ry} rz={rz}  [{profil}]")

    print(f"\nErgebnisse in: {results_dir}")


# ---------------------------------------------------------------------------
# --k2set / --k3set
# ---------------------------------------------------------------------------

def _process_kset(k_name, funcs, data_dir, results_dir, number=None, force=False):
    """Generische Verarbeitung fuer K2/K3: iteriert ueber SWITCH_FILTERS."""
    omniflo_dir = os.path.join(data_dir, "omniflo")

    for key, k_func in funcs.items():
        label, json_file, filter_func = SWITCH_FILTERS[key]

        json_path = os.path.join(data_dir, "json", json_file)
        if not os.path.exists(json_path):
            continue
        with open(json_path, "r", encoding="utf-8") as f:
            all_items = json.load(f)

        filtered = [item for item in all_items if filter_func(item)]
        if number:
            filtered = [item for item in filtered if str(item["Sivasnr"]) == str(number)]
        if not filtered:
            continue

        print(f"=== {k_name} setzen: {label} ===")
        for item in filtered:
            sivasnr = str(item["Sivasnr"])
            dxf_path = os.path.join(results_dir, f"{sivasnr}.dxf")
            if not os.path.exists(dxf_path):
                dxf_path = os.path.join(omniflo_dir, f"{sivasnr}.dxf")
            if not os.path.exists(dxf_path):
                continue

            doc = ezdxf.readfile(dxf_path)
            msp = doc.modelspace()

            if not _check_and_clean_ks(msp, k_name, sivasnr, force):
                continue

            result = k_func(doc)

            if result is None:
                continue

            point, rx, ry, rz = result
            insert_ks(msp, k_name, point, rx, ry, rz)

            out_path = os.path.join(results_dir, f"{sivasnr}.dxf")
            doc.saveas(out_path)
            print(f"  {sivasnr}: {k_name} at ({point[0]:.2f},{point[1]:.2f},{point[2]:.2f}) "
                  f"rx={rx} ry={ry} rz={rz:.1f}  [{item['ProfilTyp']}]")

    print(f"\nErgebnisse in: {results_dir}")


def process_k2set(data_dir, results_dir, number=None, force=False):
    """Setzt K2-Block (linker Bogenzweig) an alle Boegen und Weichen."""
    _process_kset("K2", K2_FUNCS, data_dir, results_dir, number, force)


def process_k3set(data_dir, results_dir, number=None, force=False):
    """Setzt K3-Block (rechter Bogenzweig) an Weichen mit 2+ Boegen."""
    _process_kset("K3", K3_FUNCS, data_dir, results_dir, number, force)


def process_k4set(data_dir, results_dir, number=None, force=False):
    """Setzt K4-Block (oberes K1-Linienende) an Weichen ohne Doppel/Dreifach/Delta."""
    omniflo_dir = os.path.join(data_dir, "omniflo")

    for key, k4_func in K4_FUNCS.items():
        label, json_file, filter_func = SWITCH_FILTERS[key]

        json_path = os.path.join(data_dir, "json", json_file)
        if not os.path.exists(json_path):
            continue
        with open(json_path, "r", encoding="utf-8") as f:
            all_items = json.load(f)

        # SWITCH_FILTERS-Filter + K4-Ausschluss (Doppelweiche etc.)
        filtered = [item for item in all_items
                    if filter_func(item)
                    and item.get("WeichenTyp", "") not in _K4_EXCLUDE_TYPES]
        if number:
            filtered = [item for item in filtered if str(item["Sivasnr"]) == str(number)]
        if not filtered:
            continue

        print(f"=== K4 setzen: {label} ===")
        for item in filtered:
            sivasnr = str(item["Sivasnr"])
            dxf_path = os.path.join(results_dir, f"{sivasnr}.dxf")
            if not os.path.exists(dxf_path):
                dxf_path = os.path.join(omniflo_dir, f"{sivasnr}.dxf")
            if not os.path.exists(dxf_path):
                continue

            doc = ezdxf.readfile(dxf_path)
            msp = doc.modelspace()

            if not _check_and_clean_ks(msp, "K4", sivasnr, force):
                continue

            result = k4_func(doc)
            if result is None:
                continue

            point, rx, ry, rz = result
            insert_ks(msp, "K4", point, rx, ry, rz)

            out_path = os.path.join(results_dir, f"{sivasnr}.dxf")
            doc.saveas(out_path)
            print(f"  {sivasnr}: K4 at ({point[0]:.2f},{point[1]:.2f},{point[2]:.2f}) "
                  f"rx={rx} ry={ry} rz={rz:.1f}  [{item['ProfilTyp']}]")

    print(f"\nErgebnisse in: {results_dir}")


# ---------------------------------------------------------------------------
# --show-omniflo
# ---------------------------------------------------------------------------

# Farben: K1=rot(1), K2=orange(30), K3=gelb(2), K4=gruen(3)
K_COLORS = {"K1": 1, "K2": 30, "K3": 2, "K4": 3}


def show_omniflo(data_dir, results_dir):
    """Uebersichts-DXF mit K-Positionen als farbige Kreuze."""
    sources, rows = build_row_layout(data_dir, results_dir)

    target = ezdxf.new(dxfversion='R2010')
    target_msp = target.modelspace()
    target.layers.add('ANNOTATION', color=7)
    target.layers.add('K_POINTS', color=1)
    target.layers.add('ROW_LABEL', color=3)

    block_counter = 0

    for row in rows:
        label_y = row['cursor_y'] + row['max_height'] / 2
        target_msp.add_mtext(
            row['label'],
            dxfattribs={
                'layer': 'ROW_LABEL',
                'char_height': TEXT_HEIGHT * 1.2,
            }
        ).set_location(insert=(-ROW_LABEL_WIDTH, label_y))

        for elem in row['elements']:
            block_name = f"BLK_{block_counter}"
            block_counter += 1

            import_element_as_block(elem['source'], target, block_name)
            target_msp.add_blockref(block_name,
                                    insert=(elem['offset_x'], elem['offset_y']))

            # K-Positionen aus dem Quell-DXF auslesen und anzeigen
            for k_name, k_color in K_COLORS.items():
                k_data = read_ks(elem['source'], k_name)
                if k_data is None:
                    continue
                kx = k_data['point'][0] + elem['offset_x']
                ky = k_data['point'][1] + elem['offset_y']
                draw_cross(target_msp, kx, ky, CROSS_SIZE, k_color, 'K_POINTS')

            text_x = elem['offset_x'] + elem['extmin'][0]
            text_y = row['cursor_y'] + elem['height'] + TEXT_MARGIN
            target_msp.add_mtext(
                elem['sivasnr'],
                dxfattribs={
                    'layer': 'ANNOTATION',
                    'char_height': TEXT_HEIGHT,
                }
            ).set_location(insert=(text_x, text_y))

    out_path = os.path.join(results_dir, "koords_uebersicht.dxf")
    target.saveas(out_path)
    print(f"Uebersicht gespeichert: {out_path}")
    print(f"  {block_counter} Elemente in {len(rows)} Reihen")


# ---------------------------------------------------------------------------
# --test
# ---------------------------------------------------------------------------

def run_test(results_dir):
    """Erzeugt Testdatei mit 4 KOS und verifiziert Ruecklesen."""
    doc = ezdxf.new(dxfversion="R2010")
    msp = doc.modelspace()

    test_cases = [
        {"name": "K1", "point": (0, 0, 0),  "rx": 0,  "ry": 0,  "rz": 0},
        {"name": "K2", "point": (10, 0, 0), "rx": 0,  "ry": 0,  "rz": 90},
        {"name": "K3", "point": (20, 0, 0), "rx": 0,  "ry": 45, "rz": 0},
        {"name": "K4", "point": (30, 0, 0), "rx": 30, "ry": 45, "rz": 60},
    ]

    for tc in test_cases:
        insert_ks(msp, tc["name"], tc["point"], tc["rx"], tc["ry"], tc["rz"])

    out_path = os.path.join(results_dir, "ks_test.dxf")
    doc.saveas(out_path)
    print(f"Testdatei gespeichert: {out_path}\n")

    print("Verifikation:")
    ok = verify_ks(out_path, test_cases)
    print(f"\nErgebnis: {'ALLE OK' if ok else 'FEHLER GEFUNDEN'}")
    return ok


# ---------------------------------------------------------------------------
# main
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(
        description="Setzt Koordinatensystem-Bloecke in Omniflo DXF-Dateien"
    )
    parser.add_argument("--k1set", action="store_true",
                        help="K1-Block an alle Boegen und Weichen setzen")
    parser.add_argument("--k2set", action="store_true",
                        help="K2-Block setzen (linker Bogenzweig)")
    parser.add_argument("--k3set", action="store_true",
                        help="K3-Block setzen (rechter Bogenzweig, nur Doppel-/Dreiwegeweichen)")
    parser.add_argument("--k4set", action="store_true",
                        help="K4-Block setzen (oberes K1-Linienende, nicht Doppel/Dreifach/Delta)")
    parser.add_argument("--set-all", action="store_true",
                        help="Alle Koordinatensysteme (K1-K4) in einem Durchgang setzen")
    parser.add_argument("--force", action="store_true",
                        help="Bestehende Kx ueberschreiben (sonst skip)")
    parser.add_argument("--show-omniflo", action="store_true",
                        help="Uebersichts-DXF mit K-Positionen erzeugen")
    parser.add_argument("--test", action="store_true",
                        help="Testdatei mit 4 KOS erzeugen und verifizieren")
    parser.add_argument("--number", type=int,
                        help="Nur diese 9-stellige Sivasnr verarbeiten")
    args = parser.parse_args()

    if args.number and len(str(args.number)) != 9:
        print("FEHLER: --number muss eine 9-stellige Ganzzahl sein.")
        sys.exit(1)

    if not args.k1set and not args.k2set and not args.k3set and not args.k4set and not args.set_all and not args.show_omniflo and not args.test:
        parser.print_help()
        sys.exit(1)

    data_dir = os.environ.get("DXFM_DATA")
    results_dir = os.environ.get("DXFM_RESULTS")

    if not data_dir or not results_dir:
        if args.test:
            results_dir = results_dir or "results"
            os.makedirs(results_dir, exist_ok=True)
            run_test(results_dir)
            return
        print("FEHLER: Umgebungsvariablen DXFM_DATA und DXFM_RESULTS muessen gesetzt sein.")
        sys.exit(1)

    os.makedirs(results_dir, exist_ok=True)

    if args.test:
        run_test(results_dir)

    if args.set_all:
        process_k1set(data_dir, results_dir, args.number, args.force)
        process_k2set(data_dir, results_dir, args.number, args.force)
        process_k3set(data_dir, results_dir, args.number, args.force)
        process_k4set(data_dir, results_dir, args.number, args.force)

    if args.k1set:
        process_k1set(data_dir, results_dir, args.number, args.force)

    if args.k2set:
        process_k2set(data_dir, results_dir, args.number, args.force)

    if args.k3set:
        process_k3set(data_dir, results_dir, args.number, args.force)

    if args.k4set:
        process_k4set(data_dir, results_dir, args.number, args.force)

    if args.show_omniflo:
        print("=== Koordinaten Uebersicht ===")
        show_omniflo(data_dir, results_dir)


if __name__ == "__main__":
    main()
