import argparse
import json
import re
import sys
from pathlib import Path
from configparser import RawConfigParser
from fnmatch import fnmatch

import ezdxf
from openpyxl import Workbook

from utils import check_environment_var, check_file_in_work


"""
translate.py – Texte aus DXF extrahieren, übersetzen und DXF-Dateien neu beschriften.

Alle Ausgabedateien landen im PROJECT_WORK Verzeichnis.
Die Übersetzungs-Configs liegen in PROJECT_TRANSLATION/<LANG>.cfg (z.B. CS.cfg, EN.cfg).
Beim Laden einer Config werden Selbstübersetzungen entfernt und Übersetzungsketten gemeldet.

───────────────────────────────────────────────────────────────────────────────
SCHRITT 1 – Texte aus DXF extrahieren und Roh-JSON erzeugen
───────────────────────────────────────────────────────────────────────────────
  -e --extract -f myfile.dxf [-t json,excel,text] [-o output]

  Liest alle TEXT/MTEXT-Objekte und Symbol-Attribute aus einer DXF/DWG-Datei,
  filtert sie über translator.cfg und schreibt eine JSON-Datei mit drei Blöcken:
    "translations": []          (leer – keine Sprache angegeben)
    "untranslated": [...]       (alle gefilterten Texte)
    "ignored":      [...]       (herausgefilterte Texte)
  Optional: zusätzliche Ausgabe als Excel oder Text (-t excel,json,text).

───────────────────────────────────────────────────────────────────────────────
SCHRITT 2a – JSON/TXT auto-übersetzen (Drop auf tr2dxf_CS.bat / tr2dxf_EN.bat)
───────────────────────────────────────────────────────────────────────────────
  --retranslate-json myfile_texts.json --translate CS[,EN,...]
  --retranslate-txt  myfile_texts.txt  --translate CS

  Liest eine bestehende JSON-Datei (Legacy-Flat oder Multilang), übersetzt
  alle Texte aus 'untranslated' automatisch und verschiebt Treffer nach
  'translations'. Bestehende Übersetzungen werden mit der aktuellen Config
  aktualisiert. Duplikate zwischen und innerhalb der Listen werden bereinigt.
  Ausgabe immer im Multilang-Format: "[CS]": { "translations": [...], ... }
  Legacy-Dateien werden dabei automatisch ins Multilang-Format konvertiert.

───────────────────────────────────────────────────────────────────────────────
SCHRITT 2b – Mehrere Sprachen gleichzeitig extrahieren und übersetzen
───────────────────────────────────────────────────────────────────────────────
  -e --extract -f myfile.dxf --translate CS,EN [-t json] [-o output]

  Wie Schritt 1, aber mit sofortiger Auto-Übersetzung in eine oder mehrere
  Sprachen. Ausgabe direkt im Multilang-Format. Wird die Ausgabedatei erneut
  mit einer weiteren Sprache erzeugt, bleiben bestehende Sprachblöcke erhalten
  (Merge): z.B. erst --translate CS, dann --translate EN → Datei enthält [CS] + [EN].

───────────────────────────────────────────────────────────────────────────────
SCHRITT 3 – Übersetzte DXF erzeugen (Drop auf tr2dxf_CS.bat / tr2dxf_EN.bat)
───────────────────────────────────────────────────────────────────────────────
  -f myfile.dxf --todxf myfile_CS.dxf --translate CS
  [-f myfile.dxf --todxf myfile_CS.dxf --translation-json custom_texts.json]

  Liest die Übersetzungstabelle aus <filename>_texts.json (oder --translation-json),
  übersetzt TEXT/MTEXT-Objekte und Symbol-Attribute direkt in der DXF-Datei
  und schreibt eine neue DXF mit den Zielsprachen-Texten.

───────────────────────────────────────────────────────────────────────────────
WEITERE EINGABEQUELLEN
───────────────────────────────────────────────────────────────────────────────
  --fromtxt  FILE.txt              Texte aus TXT-Datei lesen (ein Text pro Zeile)
  --fromjson FILE.json             Nur 'untranslated'-Block aus JSON lesen und
                                   neu übersetzen (schreibt neue Ausgabedatei)

───────────────────────────────────────────────────────────────────────────────
GEMEINSAME OPTIONEN
───────────────────────────────────────────────────────────────────────────────
  --translate LANG[,LANG2,...]     Zielsprache(n), z.B. CS  oder  EN,CS,FR
  -t / --export-type TYPE[,TYPE]   Ausgabeformat(e): json (Standard), excel, text
  -o / --outname NAME              Ausgabedateiname (ohne Pfad)
  --lang_source LANG               Quellsprache für Symbol-Attribute (Standard: DE)
  --translation-json FILE.json     Übersetzungs-JSON für --todxf (Standard: <name>_texts.json)
  -d / --debug                     Debug-Ausgabe für Pattern-Matching
"""


def _filter_self_translations(section_name: str, translations: dict[str, str]) -> dict[str, str]:
    """
    Prüft ob in einem Übersetzungs-Block auf beiden Seiten dasselbe steht (z.B. AMR ILS=AMR ILS).
    Gibt eine bereinigte Kopie zurück und gibt für jeden Fund eine Warnung aus.

    Args:
        section_name: Name des Abschnitts (für die Warnmeldung)
        translations: Zu prüfendes Dictionary

    Returns:
        Bereinigtes Dictionary ohne Selbstübersetzungen
    """
    filtered = {}
    for key, value in translations.items():
        if key == value:
            print(f"  WARNUNG [{section_name}]: Selbstübersetzung: '{key}'")
        else:
            filtered[key] = value
    return filtered


def _check_translation_chains(
        lang: str,
        multi: dict[str, str],
        trigramme: dict[str, str],
        bigramme: dict[str, str],
        single: dict[str, str]) -> None:
    """
    Prüft ob Übersetzungsketten entstehen würden: wenn die Ausgabe eines Blocks
    durch einen nachgelagerten Block weiter übersetzt würde.

    Verarbeitungsreihenfolge: multi → trigramme → bigramme → single
    - multi-Ausgaben werden gegen trigramme/bigramme/single geprüft
    - trigramme-Ausgaben werden gegen bigramme/single geprüft
    - bigramme-Ausgaben werden gegen single geprüft

    Ändert nichts, gibt nur Warnungen aus.

    Args:
        lang: Sprach-Code (für die Warnmeldung)
        multi: Multi-Phrasen-Dict
        trigramme: Trigramme-Dict
        bigramme: Bigramme-Dict
        single: Einzelwörter-Dict
    """

    def find_substring_matches(value: str, d: dict[str, str]) -> list[str]:
        """Findet alle Keys aus d, die als Teilstring in value vorkommen."""
        return [key for key in d if key in value]

    def find_single_matches(value: str) -> list[str]:
        """
        Findet alle Single-Keys, die in value vorkommen würden.
        Spiegelt die Logik von translate_text wider:
        - Direkt-Match wenn ein Leerzeichen-Token in single enthalten ist
        - Sub-Match wenn ein [\\w-]+ Teilausdruck eines Tokens in single enthalten ist
        """
        found = []
        seen = set()
        words = value.split()
        for word in words:
            if word in single:
                if word not in seen:
                    found.append(word)
                    seen.add(word)
            else:
                for m in re.finditer(r'[\w-]+', word):
                    sub_word = m.group(0)
                    if sub_word in single and sub_word not in seen:
                        found.append(sub_word)
                        seen.add(sub_word)
        return found

    chain_warnings = []

    # multi → trigramme, bigramme, single
    for orig, transl in multi.items():
        for key in find_substring_matches(transl, trigramme):
            chain_warnings.append(
                f"[multi] '{orig}' → '{transl}' → [trigramme] würde Treffer auf '{key}' erzeugen"
            )
        for key in find_substring_matches(transl, bigramme):
            chain_warnings.append(
                f"[multi] '{orig}' → '{transl}' → [bigramme] würde Treffer auf '{key}' erzeugen"
            )
        for key in find_single_matches(transl):
            chain_warnings.append(
                f"[multi] '{orig}' → '{transl}' → [single] würde Treffer auf '{key}' erzeugen"
            )

    # trigramme → bigramme, single
    for orig, transl in trigramme.items():
        for key in find_substring_matches(transl, bigramme):
            chain_warnings.append(
                f"[trigramme] '{orig}' → '{transl}' → [bigramme] würde Treffer auf '{key}' erzeugen"
            )
        for key in find_single_matches(transl):
            chain_warnings.append(
                f"[trigramme] '{orig}' → '{transl}' → [single] würde Treffer auf '{key}' erzeugen"
            )

    # bigramme → single
    for orig, transl in bigramme.items():
        for key in find_single_matches(transl):
            chain_warnings.append(
                f"[bigramme] '{orig}' → '{transl}' → [single] würde Treffer auf '{key}' erzeugen"
            )

    if chain_warnings:
        print(f"\nWARNUNG [{lang}]: {len(chain_warnings)} mögliche Übersetzungskette(n) gefunden:")
        for w in chain_warnings:
            print(f"  WARNUNG: {w}")
    else:
        print(f"  Keine Übersetzungsketten gefunden.")


def _check_leading_spaces(config_file: Path, translation_lang: str) -> None:
    """
    Liest die Config-Datei zeilenweise und prüft ob Zeilen mit einem Leerzeichen
    oder Tabulator beginnen. Solche Zeilen werden vom RawConfigParser als Fortsetzung
    der vorherigen Zeile interpretiert (zusammengezogen), was zu fehlerhaften
    Übersetzungseinträgen führt.

    Gibt für jede betroffene Zeile eine Warnung mit Zeilennummer und Inhalt aus.
    Leere Zeilen und Kommentarzeilen (# oder ;) werden ignoriert.

    Args:
        config_file: Pfad zur Config-Datei
        translation_lang: Sprach-Code (für die Warnmeldung)
    """
    relevant_sections = {'multi', 'trigramme', 'bigramme', 'single'}
    current_section = None
    warnings_found = []

    try:
        with open(config_file, 'r', encoding='utf-8') as f:
            for line_nr, line in enumerate(f, start=1):
                # Zeilenende entfernen, führende Leerzeichen NICHT entfernen
                raw_line = line.rstrip('\n\r')
                stripped = raw_line.strip()

                # Abschnitts-Header erkennen
                if stripped.startswith('[') and stripped.endswith(']'):
                    section_name = stripped[1:-1].lower()
                    current_section = section_name if section_name in relevant_sections else None
                    continue

                # Nur Zeilen innerhalb der relevanten Blöcke prüfen
                if current_section is None:
                    continue

                # Leere Zeilen und Kommentarzeilen überspringen
                if not stripped or stripped.startswith('#') or stripped.startswith(';'):
                    continue

                # Prüfe ob Zeile mit einem Whitespace-Zeichen beginnt (Leerzeichen, Tabulator,
                # non-breaking space U+00A0 usw.) - ConfigParser behandelt alle isspace()-Zeichen
                # als Fortsetzungszeichen und zieht die Zeile mit der vorherigen zusammen
                if raw_line and raw_line[0].isspace():
                    warnings_found.append((line_nr, current_section, raw_line))

    except Exception as e:
        print(f"WARNUNG: Fehler beim Prüfen auf führende Leerzeichen: {e}")
        return

    if warnings_found:
        print(f"\nWARNUNG [{translation_lang}]: {len(warnings_found)} Zeile(n) mit führendem Whitespace gefunden:")
        print(f"  (Leerzeichen, Tabulator, non-breaking space u.a. werden vom ConfigParser")
        print(f"  als Fortsetzungszeichen interpretiert und mit der vorherigen Zeile zusammengezogen!)")
        for line_nr, section, line_content in warnings_found:
            print(f"  Zeile {line_nr:4d} [{section}]: '{line_content}'")
    else:
        print(f"  Keine führenden Leerzeichen gefunden.")


def load_translation_config(translation_lang: str, translation_dir: Path) -> tuple[dict[str, str], dict[str, str], dict[str, str], dict[str, str]]:
    """
    Lädt eine Übersetzungs-Config-Datei (z.B. CS.cfg, EN.cfg, FR.cfg).

    Args:
        translation_lang: Sprach-Code (z.B. "CS", "EN", "FR")
        translation_dir: Verzeichnis mit Übersetzungs-Configs (aus PROJECT_TRANSLATION)

    Returns:
        Tuple mit (Multi-Dict, Trigramme-Dict, Bigramme-Dict, Single-Dict)
    """
    config_file = translation_dir / f"{translation_lang}.cfg"

    if not config_file.exists():
        print(f"FEHLER: Übersetzungs-Konfiguration nicht gefunden: {config_file}")
        sys.exit(1)

    # Prüfe auf führende Leerzeichen vor dem Parsen (würden Zeilen zusammenziehen)
    _check_leading_spaces(config_file, translation_lang)

    # RawConfigParser mit case-sensitive Keys und Unterstützung für Einträge ohne Wert
    # (allow_no_value=True damit der [transfer]-Abschnitt mit einzelnen Begriffen ohne '=' gelesen werden kann)
    config = RawConfigParser(allow_no_value=True)
    # Überschreibe optionxform um Groß-/Kleinschreibung zu erhalten
    config.optionxform = str
    try:
        config.read(config_file, encoding='utf-8')
    except Exception as e:
        print(f"Fehler beim Lesen der Übersetzungs-Konfiguration: {e}")
        sys.exit(1)

    multi = {}
    trigramme = {}
    bigramme = {}
    single = {}

    def _load_section(section_name: str) -> dict[str, str]:
        """Lädt eine Übersetzungssektion und warnt bei Einträgen ohne '=' (value=None)."""
        result = {}
        if not config.has_section(section_name):
            return result
        for key in config[section_name]:
            translation = config[section_name][key]
            if translation is None:
                print(f"  WARNUNG [{translation_lang}] [{section_name}]: Eintrag ohne '=' ignoriert: '{key}'")
                continue
            if translation and not translation.startswith('#'):
                result[key] = translation
        return result

    # Lade Multi (4+ Wörter)
    multi = _load_section('multi')

    # Lade Trigramme
    trigramme = _load_section('trigramme')

    # Lade Bigramme
    bigramme = _load_section('bigramme')

    # Lade Single
    single = _load_section('single')

    # Prüfe Selbstübersetzungen in jedem Block und entferne sie mit Warnung
    multi = _filter_self_translations('multi', multi)
    trigramme = _filter_self_translations('trigramme', trigramme)
    bigramme = _filter_self_translations('bigramme', bigramme)
    single = _filter_self_translations('single', single)

    print(f"\nÜbersetzungs-Config geladen ({translation_lang}):")
    print(f"  Multi-Phrasen: {len(multi)}")
    print(f"  Trigramme: {len(trigramme)}")
    print(f"  Bigramme: {len(bigramme)}")
    print(f"  Einzelwörter: {len(single)}")

    # Prüfe Übersetzungsketten zwischen den Blöcken
    _check_translation_chains(translation_lang, multi, trigramme, bigramme, single)

    return multi, trigramme, bigramme, single


def load_transfer_terms(translation_lang: str, translation_dir: Path) -> set[str]:
    """
    Lädt die Transfer-Begriffe aus einer Übersetzungs-Config-Datei (Abschnitt [transfer]).

    Transfer-Begriffe werden beim Übersetzen von Symbol-Attributen (Block-Inserts) ohne
    Übersetzung direkt in das Ziel-Attribut übernommen. Für TEXT/MTEXT-Objekte gilt
    diese Regel nicht.

    Einträge im Abschnitt [transfer] stehen einzeln pro Zeile (ohne Wert/Gleichheitszeichen).

    Args:
        translation_lang: Sprach-Code (z.B. "CS", "EN", "FR")
        translation_dir: Verzeichnis mit Übersetzungs-Configs (aus PROJECT_TRANSLATION)

    Returns:
        Set mit den Transfer-Begriffen (Groß-/Kleinschreibung wie in der Config)
    """
    config_file = translation_dir / f"{translation_lang}.cfg"

    if not config_file.exists():
        return set()

    config = RawConfigParser(allow_no_value=True)
    config.optionxform = str
    try:
        config.read(config_file, encoding='utf-8')
    except Exception as e:
        print(f"Warnung: Fehler beim Lesen der Transfer-Begriffe aus {config_file}: {e}")
        return set()

    transfer: set[str] = set()
    if config.has_section('transfer'):
        for key in config['transfer']:
            term = key.strip()
            if term and not term.startswith('#'):
                transfer.add(term)

    if transfer:
        print(f"  Transfer-Begriffe ({translation_lang}): {len(transfer)} ({', '.join(sorted(transfer))})")

    return transfer


def _load_translator_config(cfg_path: Path) -> RawConfigParser:
    """
    Lädt translator.cfg aus cfg_path und gibt einen RawConfigParser zurück.
    Bricht mit Fehlermeldung ab wenn die Datei nicht existiert oder nicht gelesen werden kann.

    Args:
        cfg_path: Pfad zum Konfigurationsverzeichnis (PROJECT_CFG)

    Returns:
        Gelesener RawConfigParser (case-sensitive Keys)
    """
    config_file = cfg_path / "translator.cfg"

    if not config_file.exists():
        print(f"FEHLER: Konfigurationsdatei nicht gefunden: {config_file}")
        print(f"Bitte erstellen Sie die Datei 'translator.cfg' im Verzeichnis: {cfg_path}")
        sys.exit(1)

    config = RawConfigParser()
    config.optionxform = str
    try:
        config.read(config_file, encoding='utf-8')
    except Exception as e:
        print(f"Fehler beim Lesen der Konfigurationsdatei: {e}")
        sys.exit(1)

    return config


def load_symbol_attribute_mapping(cfg_path: Path) -> dict[str, str]:
    """
    Liest die Zuordnung von Sprachcodes zu Symbol-Attributnamen aus der translator.cfg Datei.

    Verwendet den Abschnitt [symbol_attribute], z.B.:
      DE = TEXT-D
      EN = TEXT-E
      CS = TEXT-CS

    Args:
        cfg_path: Pfad zum Konfigurationsverzeichnis (PROJECT_CFG)

    Returns:
        Dictionary mit Sprachcode -> Attributname Mapping (z.B. "DE" -> "TEXT-D", "EN" -> "TEXT-EN")
    """
    config = _load_translator_config(cfg_path)

    symbol_mapping: dict[str, str] = {}

    # Lade Symbol-Attribut-Zuordnung
    if config.has_section('symbol_attribute'):
        for key in config['symbol_attribute']:
            attr_name = config['symbol_attribute'][key].strip()
            if attr_name and not attr_name.startswith('#'):
                symbol_mapping[key] = attr_name

    print(f"\nGeladene Symbol-Attribut-Zuordnungen: {len(symbol_mapping)}")
    for lang, attr in symbol_mapping.items():
        print(f"  {lang} -> {attr}")

    return symbol_mapping


def load_symbol_layer_mapping(cfg_path: Path) -> dict[str, str]:
    """
    Liest die Zuordnung von Sprachcodes zu Symbol-Layern aus der translator.cfg Datei.

    Verwendet den Abschnitt [symbol_layer], z.B.:
      DE = 0-0_TEXT-D
      EN = 0-0_TEXT-E
      CS = 0-0_TEXT-CS

    Args:
        cfg_path: Pfad zum Konfigurationsverzeichnis (PROJECT_CFG)

    Returns:
        Dictionary mit Sprachcode -> Layername Mapping
    """
    config = _load_translator_config(cfg_path)

    layer_mapping: dict[str, str] = {}

    if config.has_section('symbol_layer'):
        for key in config['symbol_layer']:
            layer_name = config['symbol_layer'][key].strip()
            if layer_name and not layer_name.startswith('#'):
                layer_mapping[key] = layer_name

    print(f"\nGeladene Symbol-Layer-Zuordnungen: {len(layer_mapping)}")
    for lang, layer in layer_mapping.items():
        print(f"  {lang} -> {layer}")

    return layer_mapping


def load_ignore_patterns(cfg_path: Path) -> tuple[list[str], list[str]]:
    """
    Liest die ignore_pattern aus der translator.cfg Datei.
    Unterstützt sowohl Wildcard-Muster (*,?) als auch reguläre Ausdrücke.

    Args:
        cfg_path: Pfad zum Konfigurationsverzeichnis (PROJECT_CFG)

    Returns:
        Tuple mit (Wildcard-Mustern, Regex-Mustern)
    """
    # RawConfigParser verhindert dass Backslashes in Regex-Mustern escaped werden
    config = _load_translator_config(cfg_path)

    wildcard_patterns = []
    regex_patterns = []

    # Lade Wildcard-Muster
    if config.has_section('ignore_pattern_wildcard'):
        for key in config['ignore_pattern_wildcard']:
            pattern = config['ignore_pattern_wildcard'][key].strip()
            if pattern and not pattern.startswith('#'):
                wildcard_patterns.append(pattern)

    # Lade Regex-Muster
    if config.has_section('ignore_pattern_regex'):
        for key in config['ignore_pattern_regex']:
            pattern = config['ignore_pattern_regex'][key].strip()
            if pattern and not pattern.startswith('#'):
                regex_patterns.append(pattern)

    print(f"Geladene Wildcard-Muster: {len(wildcard_patterns)}")
    for pattern in wildcard_patterns:
        print(f"  - {pattern}")

    print(f"Geladene Regex-Muster: {len(regex_patterns)}")
    for pattern in regex_patterns:
        print(f"  - {pattern}")

    return wildcard_patterns, regex_patterns


def translate_text(text: str, multi: dict[str, str], trigramme: dict[str, str], bigramme: dict[str, str], single: dict[str, str], transfer_terms: set[str] | None = None) -> str:
    """
    Übersetzt einen Text durch Ersetzen von Multi-Phrasen, Trigrammen, Bigrammen und Einzelwörtern.
    Ersetzungen erfolgen in dieser Reihenfolge: Transfer → Multi → Trigramme → Bigramme → Einzelwörter.

    Bei Einzelwörtern werden auch Satzzeichen und eingebettete Wörter berücksichtigt:
    - "Weight:" wird zu "Hmotnost:" (Doppelpunkt bleibt erhalten)
    - "(Sub-distributor)" wird zu "(Rozvaděč)" (Klammern bleiben erhalten)
    - "\\fArial Black|b0|i0;Firewall 1" wird zu "\\fArial Black|b0|i0;Požární 1"
      (alle Wörter in einem Token werden durchsucht, Trennzeichen bleiben erhalten)
    Bindestriche im Wort (wie "Sub-distributor") bleiben Teil des Kern-Worts.

    Transfer-Begriffe werden 1:1 übernommen (ohne Übersetzung) wenn der Text exakt übereinstimmt.

    Args:
        text: Zu übersetzender Text
        multi: Dictionary mit Multi-Wort-Phrasen (4+ Wörter)
        trigramme: Dictionary mit Drei-Wort-Phrasen
        bigramme: Dictionary mit Zwei-Wort-Phrasen
        single: Dictionary mit Einzelwörtern
        transfer_terms: Set mit Begriffen die 1:1 übernommen werden (aus [transfer] Sektion)

    Returns:
        Übersetzter Text (leer wenn keine Übersetzung gefunden wurde)
    """
    # 0. Prüfe Transfer-Begriffe (exakte Übereinstimmung, 1:1 Übernahme)
    if transfer_terms is not None and text in transfer_terms:
        return text

    translated_text = text
    has_translation = False

    # 1. Multi-Phrasen ersetzen (längste Phrasen zuerst)
    for original, translation in multi.items():
        if original in translated_text:
            translated_text = translated_text.replace(original, translation)
            has_translation = True

    # 2. Trigramme ersetzen
    for original, translation in trigramme.items():
        if original in translated_text:
            translated_text = translated_text.replace(original, translation)
            has_translation = True

    # 3. Bigramme ersetzen
    for original, translation in bigramme.items():
        if original in translated_text:
            translated_text = translated_text.replace(original, translation)
            has_translation = True

    # 4. Einzelwörter ersetzen
    words = translated_text.split()
    translated_words = []
    for word in words:
        if word in single:
            # Direkter Match
            translated_words.append(single[word])
            has_translation = True
        else:
            # Suche ALLE Wörter im Token und ersetze sie (z.B. "Black|b0|i0;Firewall" enthält "Black" und "Firewall")
            # Pattern matcht Wörter (Buchstaben, Zahlen, Unterstriche, Bindestriche)
            word_found = False

            def replace_word(match):
                nonlocal word_found, has_translation
                matched_word = match.group(0)
                if matched_word in single:
                    word_found = True
                    has_translation = True
                    return single[matched_word]
                return matched_word

            # Ersetze alle gefundenen Wörter im Token
            translated_word = re.sub(r'[\w-]+', replace_word, word)
            translated_words.append(translated_word)
    translated_text = ' '.join(translated_words)

    # Gib nur übersetzten Text zurück wenn tatsächlich etwas übersetzt wurde
    return translated_text if has_translation else ""


def should_ignore_text(text: str, wildcard_patterns: list[str], regex_patterns: list[str], debug: bool = False) -> bool:
    """
    Prüft ob ein Text ignoriert werden soll basierend auf den Mustern.

    Args:
        text: Zu prüfender Text
        wildcard_patterns: Liste der Wildcard-Muster (*,?)
        regex_patterns: Liste der regulären Ausdrücke
        debug: Wenn True, gibt Debug-Informationen aus

    Returns:
        True wenn Text ignoriert werden soll, False sonst
    """
    # Prüfe Wildcard-Muster
    for pattern in wildcard_patterns:
        if fnmatch(text, pattern):
            if debug:
                print(f"  DEBUG: '{text}' matched wildcard '{pattern}'")
            return True

    # Prüfe Regex-Muster
    for pattern in regex_patterns:
        try:
            if re.search(pattern, text):
                if debug:
                    print(f"  DEBUG: '{text}' matched regex '{pattern}'")
                return True
        except re.error:
            # Falls ungültiger Regex, ignoriere dieses Muster
            print(f"WARNUNG: Ungültiger regulärer Ausdruck: {pattern}")
            continue

    return False


def extract_text_from_dxf(filename: Path, text_attr_name: str = 'TEXT-D') -> list[str]:
    """
    Extrahiert alle TEXT und MTEXT Objekte sowie TEXT-{attr} Attribute aus Blöcken einer DXF Datei.
    Verwendet ein Set, um Duplikate zu vermeiden.
    Filtert leere Texte heraus.

    Args:
        filename: Pfad zur DXF Datei
        text_attr_name: Name des Text-Attributs in Blöcken (z.B. "TEXT-D", "TEXT-EN", "TEXT-CS"). Standard: "TEXT-D"

    Returns:
        Alphabetisch sortierte Liste aller gefundenen Texte (ohne Duplikate, ohne leere Texte)
    """
    texts_set = set()  # Set für eindeutige Texte
    text_count = 0
    mtext_count = 0
    block_count = 0
    empty_count = 0

    # Attribute die ein Block haben muss, damit das Text-Attribut extrahiert wird
    required_attributes = {'IO', 'ID', 'VERW', 'BEZEICHNUNG', 'KENNZEICHNUNG', text_attr_name}

    try:
        doc = ezdxf.readfile(filename)
        msp = doc.modelspace()

        # Sammle alle TEXT Objekte
        for entity in msp.query('TEXT'):
            if entity.dxf.text:
                text_count += 1
                text = entity.dxf.text.strip()  # Entferne führende/nachfolgende Leerzeichen

                # Ignoriere leere Texte
                if not text:
                    empty_count += 1
                    continue

                texts_set.add(text)

        # Sammle alle MTEXT Objekte
        for entity in msp.query('MTEXT'):
            if entity.text:
                mtext_count += 1
                text = entity.text.strip()  # Entferne führende/nachfolgende Leerzeichen

                # Ignoriere leere Texte
                if not text:
                    empty_count += 1
                    continue

                texts_set.add(text)

        # Sammle TEXT-D aus Blöcken mit den erforderlichen Attributen
        for entity in msp.query('INSERT'):
            if entity.has_attrib:
                # Sammle alle vorhandenen Attribute des Blocks
                block_attributes = set()
                text_d_value = None

                for attrib in entity.attribs:
                    block_attributes.add(attrib.dxf.tag)
                    if attrib.dxf.tag == text_attr_name:
                        text_d_value = attrib.dxf.text.strip() if attrib.dxf.text else None

                # Prüfe ob Block alle erforderlichen Attribute hat
                if required_attributes.issubset(block_attributes) and text_d_value:
                    block_count += 1
                    texts_set.add(text_d_value)

        # Konvertiere Set zu sortierter Liste
        texts = sorted(texts_set)

        print(f"Anzahl gefundener TEXT Objekte: {text_count}")
        print(f"Anzahl gefundener MTEXT Objekte: {mtext_count}")
        print(f"Anzahl gefundener {text_attr_name} in Blöcken: {block_count}")
        print(f"Leere Texte ignoriert: {empty_count}")
        total_count = text_count + mtext_count + block_count
        print(f"Duplikate entfernt: {total_count - empty_count - len(texts)}")
        print(f"Eindeutige Texte extrahiert: {len(texts)}")

    except Exception as e:
        print(f"Fehler beim Lesen der DXF Datei: {e}")
        sys.exit(1)

    return texts


def filter_texts(texts: list[str], wildcard_patterns: list[str], regex_patterns: list[str], debug: bool = False) -> tuple[list[str], list[str]]:
    """
    Filtert eine Liste von Texten basierend auf Wildcard- und Regex-Mustern.
    Diese Funktion ist unabhängig von der Quelle der Texte (DXF oder TXT).

    Args:
        texts: Liste der zu filternden Texte
        wildcard_patterns: Liste der Wildcard-Muster zum Filtern
        regex_patterns: Liste der Regex-Muster zum Filtern
        debug: Wenn True, gibt Debug-Informationen aus

    Returns:
        Tuple mit (akzeptierte Texte, ignorierte Texte) - beide alphabetisch sortiert
    """
    accepted_texts = []
    ignored_texts = []

    for text in texts:
        if should_ignore_text(text, wildcard_patterns, regex_patterns, debug):
            ignored_texts.append(text)
        else:
            accepted_texts.append(text)

    # Sortiere beide Listen alphabetisch
    accepted_texts.sort()
    ignored_texts.sort()

    print(f"\nFilterung abgeschlossen:")
    print(f"  Akzeptierte Texte: {len(accepted_texts)}")
    print(f"  Ignorierte Texte: {len(ignored_texts)}")

    return accepted_texts, ignored_texts


def read_texts_from_file(filename: Path) -> list[str]:
    """
    Liest Texte aus einer UTF-8 Textdatei (ein Text pro Zeile).
    Entfernt Duplikate und leere Zeilen.
    Ignoriert Abschnitts-Marker (Zeilen die mit "---" beginnen).

    Args:
        filename: Pfad zur Textdatei

    Returns:
        Alphabetisch sortierte Liste aller Texte (ohne Duplikate, ohne leere Zeilen)
    """
    texts_set = set()

    try:
        with open(filename, 'r', encoding='utf-8') as f:
            for line in f:
                text = line.strip()
                if text:  # Ignoriere leere Zeilen
                    # Ignoriere Abschnitts-Marker (z.B. "--- translations:", "--- ignored:")
                    if text.startswith('---'):
                        continue
                    # Konvertiere escaped Zeilenumbrüche zurück
                    text = text.replace('\\n', '\n').replace('\\r', '\r')
                    texts_set.add(text)

        texts = sorted(texts_set)
        print(f"Texte aus Datei gelesen: {len(texts)}")
        return texts

    except Exception as e:
        print(f"Fehler beim Lesen der Textdatei: {e}")
        sys.exit(1)


def detect_json_structure(data: dict) -> tuple[str, list[str]]:
    """
    Erkennt die Struktur einer JSON-Datei (alte oder hierarchische mehrsprachige Struktur).

    Args:
        data: Geladene JSON-Daten als Dictionary

    Returns:
        Tuple mit (Struktur-Typ, Liste der Sprachen)
        - Struktur-Typ: "legacy" (alte Struktur) oder "multilang" (mehrsprachige hierarchische Struktur)
        - Liste der Sprachen: leer bei legacy, z.B. ["EN", "FR", "CS"] bei multilang
    """
    # Prüfe ob alte Struktur (hat 'untranslated' oder 'translations' Keys)
    if 'untranslated' in data or 'translations' in data:
        return "legacy", []

    # Prüfe ob neue hierarchische Struktur (hat [LANG] Keys)
    languages = []
    for key in data.keys():
        if key.startswith('[') and key.endswith(']') and len(key) > 2:
            # Extrahiere Sprachcode aus [LANG]
            lang = key[1:-1]
            languages.append(lang)

    if languages:
        return "multilang", languages

    # Unbekannte Struktur
    return "unknown", []


def read_texts_from_json(filename: Path) -> list[str]:
    """
    Liest Texte aus einer JSON-Datei.
    Unterstützt beide Strukturen:
    - Alte Struktur: "untranslated": [{"source": "...", "target": ""}, ...]
    - Hierarchische Struktur: "[LANG]": {"untranslated": [...], ...}

    Args:
        filename: Pfad zur JSON-Datei

    Returns:
        Alphabetisch sortierte Liste aller "source" Texte aus den untranslated Blöcken
    """
    try:
        with open(filename, 'r', encoding='utf-8') as f:
            data = json.load(f)

        if not isinstance(data, dict):
            print(f"FEHLER: JSON-Datei hat ungültige Struktur (erwartet Object, bekam {type(data).__name__})")
            sys.exit(1)

        # Erkenne Struktur
        structure_type, languages = detect_json_structure(data)

        texts_set = set()  # Verwende Set um Duplikate zu vermeiden

        if structure_type == "legacy":
            # Alte Struktur
            if 'untranslated' not in data:
                print(f"FEHLER: JSON-Datei enthält keinen 'untranslated' Block")
                sys.exit(1)

            untranslated = data['untranslated']
            if not isinstance(untranslated, list):
                print(f"FEHLER: 'untranslated' Block hat ungültige Struktur (erwartet Array)")
                sys.exit(1)

            for idx, entry in enumerate(untranslated):
                if not isinstance(entry, dict):
                    print(f"WARNUNG: Eintrag {idx} in 'untranslated' ist kein Object, überspringe")
                    continue

                if 'source' not in entry:
                    print(f"WARNUNG: Eintrag {idx} in 'untranslated' hat kein 'source' Feld, überspringe")
                    continue

                source_text = entry['source']
                if source_text:  # Ignoriere leere Texte
                    texts_set.add(source_text)

            print(f"Texte aus JSON-Datei gelesen (alte Struktur): {len(texts_set)}")

        elif structure_type == "multilang":
            # Hierarchische mehrsprachige Struktur
            print(f"Erkannte mehrsprachige Struktur mit Sprachen: {', '.join(languages)}")

            for lang in languages:
                lang_key = f"[{lang}]"
                if lang_key not in data:
                    continue

                lang_data = data[lang_key]
                if not isinstance(lang_data, dict):
                    print(f"WARNUNG: [{lang}] Block ist kein Object, überspringe")
                    continue

                # Lese aus untranslated Block
                if 'untranslated' in lang_data:
                    untranslated = lang_data['untranslated']
                    if not isinstance(untranslated, list):
                        print(f"WARNUNG: [{lang}][untranslated] ist kein Array, überspringe")
                        continue

                    for idx, entry in enumerate(untranslated):
                        if not isinstance(entry, dict):
                            continue

                        if 'source' not in entry:
                            continue

                        source_text = entry['source']
                        if source_text:
                            texts_set.add(source_text)

            print(f"Texte aus JSON-Datei gelesen (mehrsprachige Struktur): {len(texts_set)}")

        else:
            print(f"FEHLER: JSON-Datei hat unbekannte Struktur")
            sys.exit(1)

        # Konvertiere Set zu sortierter Liste
        texts = sorted(texts_set)
        return texts

    except json.JSONDecodeError as e:
        print(f"Fehler beim Parsen der JSON-Datei: {e}")
        sys.exit(1)
    except Exception as e:
        print(f"Fehler beim Lesen der JSON-Datei: {e}")
        sys.exit(1)


def load_translations_from_json(filename: Path, target_lang: str = None) -> dict[str, str]:
    """
    Liest Übersetzungen aus einer JSON-Datei.
    Unterstützt beide Strukturen:
    - Alte Struktur: "translations": [{"source": "...", "target": "..."}, ...]
    - Hierarchische Struktur: "[LANG]": {"translations": [...], ...}

    Bei hierarchischer Struktur werden Übersetzungen aus allen Sprachen oder nur der angegebenen Sprache gelesen.
    Wenn mehrere Übersetzungen für denselben source existieren, wird die letzte verwendet (Sprache mit höherer Priorität).

    Args:
        filename: Pfad zur JSON-Datei
        target_lang: Optional: Bei hierarchischer Struktur nur Übersetzungen dieser Sprache laden

    Returns:
        Dictionary mit source -> target Mappings
    """
    try:
        with open(filename, 'r', encoding='utf-8') as f:
            data = json.load(f)

        if not isinstance(data, dict):
            print(f"FEHLER: JSON-Datei hat ungültige Struktur (erwartet Object, bekam {type(data).__name__})")
            sys.exit(1)

        # Erkenne Struktur
        structure_type, languages = detect_json_structure(data)

        translations = {}

        if structure_type == "legacy":
            # Alte Struktur
            if 'translations' not in data:
                print(f"FEHLER: JSON-Datei enthält keinen 'translations' Block")
                sys.exit(1)

            translations_list = data['translations']
            if not isinstance(translations_list, list):
                print(f"FEHLER: 'translations' Block hat ungültige Struktur (erwartet Array)")
                sys.exit(1)

            for idx, entry in enumerate(translations_list):
                if not isinstance(entry, dict):
                    print(f"WARNUNG: Eintrag {idx} in 'translations' ist kein Object, überspringe")
                    continue

                if 'source' not in entry or 'target' not in entry:
                    print(f"WARNUNG: Eintrag {idx} in 'translations' fehlt 'source' oder 'target', überspringe")
                    continue

                source = entry['source']
                target = entry['target']
                if source and target:  # Ignoriere leere Einträge
                    translations[source] = target

            print(f"Übersetzungen aus JSON-Datei geladen (alte Struktur): {len(translations)}")

        elif structure_type == "multilang":
            # Hierarchische mehrsprachige Struktur
            print(f"Erkannte mehrsprachige Struktur mit Sprachen: {', '.join(languages)}")

            # Filtere Sprachen falls target_lang angegeben
            langs_to_load = [target_lang] if target_lang and target_lang in languages else languages

            for lang in langs_to_load:
                lang_key = f"[{lang}]"
                if lang_key not in data:
                    continue

                lang_data = data[lang_key]
                if not isinstance(lang_data, dict):
                    print(f"WARNUNG: [{lang}] Block ist kein Object, überspringe")
                    continue

                # Lese aus translations Block (oder translated für Rückwärtskompatibilität)
                translations_key = 'translations' if 'translations' in lang_data else 'translated'
                if translations_key in lang_data:
                    translations_list = lang_data[translations_key]
                    if not isinstance(translations_list, list):
                        print(f"WARNUNG: [{lang}][{translations_key}] ist kein Array, überspringe")
                        continue

                    for idx, entry in enumerate(translations_list):
                        if not isinstance(entry, dict):
                            continue

                        if 'source' not in entry or 'target' not in entry:
                            continue

                        source = entry['source']
                        target = entry['target']
                        if source and target:
                            translations[source] = target

            if target_lang:
                print(f"Übersetzungen für Sprache [{target_lang}] aus JSON-Datei geladen: {len(translations)}")
            else:
                print(f"Übersetzungen aus JSON-Datei geladen (mehrsprachige Struktur, alle Sprachen): {len(translations)}")

        else:
            print(f"FEHLER: JSON-Datei hat unbekannte Struktur")
            sys.exit(1)

        return translations

    except json.JSONDecodeError as e:
        print(f"Fehler beim Parsen der JSON-Datei: {e}")
        sys.exit(1)
    except Exception as e:
        print(f"Fehler beim Lesen der JSON-Datei: {e}")
        sys.exit(1)


def ensure_block_attdef_and_layer(doc, block_name: str,
                                  text_source_attr: str,
                                  text_target_attr: str,
                                  target_layer: str | None = None) -> None:
    """
    Stellt sicher, dass im Block-Definitionseintrag ein ATTDEF für das Ziel-Attribut
    existiert und der passende Layer vorhanden ist.

    - Wenn der Layer aus translator.cfg ([symbol_layer]) noch nicht existiert, wird er angelegt.
    - Wenn das ATTDEF für text_target_attr noch nicht existiert, wird es auf Basis des
      ATTDEF von text_source_attr erzeugt (Position, Höhe, Stil, Rotation werden übernommen).
    """
    if doc is None or not block_name:
        return

    try:
        block = doc.blocks[block_name]
    except KeyError:
        # Kein Block mit diesem Namen vorhanden (z.B. anonyme Blöcke) -> nichts zu tun
        return

    # Layer sicherstellen
    if target_layer:
        if target_layer not in doc.layers:
            doc.layers.new(name=target_layer)

    source_def = None
    target_def = None

    for attdef in block.attdefs():
        tag = attdef.dxf.tag
        if tag == text_target_attr:
            target_def = attdef
        elif tag == text_source_attr:
            source_def = attdef

    # Ziel-ATTDEF existiert bereits -> ggf. nur Layer korrigieren
    if target_def is not None:
        if target_layer:
            target_def.dxf.layer = target_layer
        return

    # Neues ATTDEF anlegen, möglichst anhand der Quell-Definition
    insert = (0.0, 0.0)
    height = 2.5
    layer_name = target_layer or "0"

    if source_def is not None:
        insert = source_def.dxf.insert
        if source_def.dxf.hasattr('height'):
            height = source_def.dxf.height
        if source_def.dxf.hasattr('layer') and not target_layer:
            layer_name = source_def.dxf.layer

    # `add_attdef` unterstützt keinen `layer`-Parameter direkt, daher Layer danach setzen
    new_def = block.add_attdef(
        tag=text_target_attr,
        insert=insert,
        height=height,
    )
    new_def.dxf.layer = layer_name

    # Weitere Eigenschaften (Rotation, Stil) vom Quell-ATTDEF übernehmen
    if source_def is not None:
        if source_def.dxf.hasattr('rotation'):
            new_def.dxf.rotation = source_def.dxf.rotation
        if source_def.dxf.hasattr('style'):
            new_def.dxf.style = source_def.dxf.style


def translate_block_attribute(entity,
                              doc,
                              translations: dict[str, str],
                              wildcard_patterns: list[str],
                              regex_patterns: list[str],
                              required_attributes: set[str],
                              text_source_attr: str,
                              text_target_attr: str,
                              target_layer: str | None = None,
                              transfer_terms: set[str] | None = None) -> tuple[int, int, int, int]:
    """
    Übersetzt Text-Attribute eines einzelnen Block-Inserts (INSERT entity).

    Args:
        entity: INSERT entity aus der DXF-Datei
        doc: ezdxf-Dokument, um Blockdefinitionen und Layer zu ändern
        translations: Dictionary mit source -> target Mappings
        wildcard_patterns: Wildcard-Muster zum Filtern
        regex_patterns: Regex-Muster zum Filtern
        required_attributes: Set mit erforderlichen Attributnamen für den Block
        text_source_attr: Quell-Attributname (z.B. "TEXT-D", "TEXT-EN")
        text_target_attr: Ziel-Attributname (z.B. "TEXT-EN", "TEXT-CS")
        target_layer: Ziel-Layername für das Ziel-Attribut (aus [symbol_layer])
        transfer_terms: Begriffe die ohne Übersetzung direkt übernommen werden (aus [transfer])

    Returns:
        Tuple mit (translated_count, skipped_count, ignored_count, transferred_count) für diesen Block
    """
    if not entity.has_attrib:
        return 0, 0, 0, 0

    # Sammle alle vorhandenen Attribute des Blocks
    block_attributes = {}
    text_source_attrib = None
    text_target_attrib = None

    for attrib in entity.attribs:
        block_attributes[attrib.dxf.tag] = attrib
        if attrib.dxf.tag == text_source_attr:
            text_source_attrib = attrib
        elif attrib.dxf.tag == text_target_attr:
            text_target_attrib = attrib

    # Prüfe ob Block alle erforderlichen Attribute hat
    if not (required_attributes.issubset(block_attributes.keys()) and text_source_attrib):
        return 0, 0, 0, 0

    original_text = text_source_attrib.dxf.text.strip() if text_source_attrib.dxf.text else None

    if not original_text:
        return 0, 0, 0, 0

    # Prüfe ob Text ignoriert werden soll
    if should_ignore_text(original_text, wildcard_patterns, regex_patterns):
        return 0, 0, 1, 0

    # Bestimme Zieltext: Transfer (1:1 übernehmen) oder Übersetzung
    is_transfer = transfer_terms is not None and original_text in transfer_terms
    if is_transfer:
        target_text = original_text
    elif original_text in translations:
        target_text = translations[original_text]
    else:
        return 0, 1, 0, 0

    # Schreibe Zieltext in Ziel-Attribut
    if text_target_attrib:
        # Ziel-Attribut existiert bereits, aktualisiere es
        text_target_attrib.dxf.text = target_text
        # Layer ggf. anpassen
        if target_layer:
            text_target_attrib.dxf.layer = target_layer
    else:
        # Vor dem Anlegen sicherstellen, dass im Block ein passender ATTDEF existiert
        ensure_block_attdef_and_layer(doc, entity.dxf.name, text_source_attr, text_target_attr, target_layer)

        # Ziel-Attribut existiert nicht, erstelle es basierend auf Quell-Attribut
        new_attrib = entity.add_attrib(
            tag=text_target_attr,
            text=target_text,
            insert=text_source_attrib.dxf.insert,
        )
        # Kopiere weitere Eigenschaften
        if text_source_attrib.dxf.hasattr('height'):
            new_attrib.dxf.height = text_source_attrib.dxf.height
        if text_source_attrib.dxf.hasattr('style'):
            new_attrib.dxf.style = text_source_attrib.dxf.style
        if text_source_attrib.dxf.hasattr('rotation'):
            new_attrib.dxf.rotation = text_source_attrib.dxf.rotation
        if text_source_attrib.dxf.hasattr('width'):
            new_attrib.dxf.width = text_source_attrib.dxf.width
        # Layer explizit auf Ziel-Layer setzen, falls konfiguriert, sonst wie Quell-Attribut
        if target_layer:
            new_attrib.dxf.layer = target_layer
        elif text_source_attrib.dxf.hasattr('layer'):
            new_attrib.dxf.layer = text_source_attrib.dxf.layer

    if is_transfer:
        return 0, 0, 0, 1
    return 1, 0, 0, 0


def _translate_text_entities(msp, entity_type: str,
                             translations: dict[str, str],
                             wildcard_patterns: list[str],
                             regex_patterns: list[str]) -> tuple[int, int, int]:
    """
    Übersetzt alle TEXT oder MTEXT Entitäten im Modelraum in-place.

    Args:
        msp: ezdxf Modelspace-Objekt
        entity_type: "TEXT" oder "MTEXT"
        translations: Dictionary mit source -> target Mappings
        wildcard_patterns: Wildcard-Muster zum Filtern
        regex_patterns: Regex-Muster zum Filtern

    Returns:
        Tuple (translated, skipped, ignored)
    """
    translated = 0
    skipped = 0
    ignored = 0

    for entity in msp.query(entity_type):
        raw_text = entity.dxf.text if entity_type == 'TEXT' else entity.text
        if not raw_text:
            continue

        original_text = raw_text.strip()

        if should_ignore_text(original_text, wildcard_patterns, regex_patterns):
            ignored += 1
            continue

        if original_text in translations:
            if entity_type == 'TEXT':
                entity.dxf.text = translations[original_text]
            else:
                entity.text = translations[original_text]
            translated += 1
        else:
            skipped += 1

    return translated, skipped, ignored


def translate_dxf_texts(input_dxf: Path,
                        output_dxf: Path,
                        translations: dict[str, str],
                        wildcard_patterns: list[str],
                        regex_patterns: list[str],
                        text_source_attr: str = 'TEXT-D',
                        text_target_attr: str = 'TEXT-CS',
                        target_layer: str | None = None,
                        transfer_terms: set[str] | None = None):
    """
    Übersetzt alle TEXT, MTEXT Objekte und Text-Attribute in Blöcken einer DXF-Datei.

    Für Block-Attribute:
    - Liest die Übersetzung aus dem Quell-Attribut (z.B. TEXT-D)
    - Schreibt die Übersetzung in das Ziel-Attribut (z.B. TEXT-EN, TEXT-CS) (erstellt das Attribut falls nicht vorhanden)
    - Das Quell-Attribut bleibt unverändert (Original wird bewahrt)
    - Begriffe aus transfer_terms werden ohne Übersetzung direkt übernommen (nur bei Block-Attributen)

    Args:
        input_dxf: Pfad zur Quell-DXF
        output_dxf: Pfad zur Ziel-DXF
        translations: Dictionary mit source -> target Mappings
        wildcard_patterns: Wildcard-Muster zum Filtern
        regex_patterns: Regex-Muster zum Filtern
        text_source_attr: Quell-Attributname für Texte in Blöcken (z.B. "TEXT-D", "TEXT-EN"). Standard: "TEXT-D"
        text_target_attr: Ziel-Attributname für Übersetzungen in Blöcken (z.B. "TEXT-EN", "TEXT-CS"). Standard: "TEXT-CS"
        transfer_terms: Begriffe die in Symbol-Attributen ohne Übersetzung übernommen werden (aus [transfer])
    """
    # Attribute die ein Block haben muss, damit das Text-Attribut übersetzt wird
    required_attributes = {'IO', 'ID', 'VERW', 'BEZEICHNUNG', 'KENNZEICHNUNG', text_source_attr}

    try:
        doc = ezdxf.readfile(input_dxf)
        msp = doc.modelspace()

        text_translated, text_skipped, text_ignored = _translate_text_entities(
            msp, 'TEXT', translations, wildcard_patterns, regex_patterns)
        mtext_translated, mtext_skipped, mtext_ignored = _translate_text_entities(
            msp, 'MTEXT', translations, wildcard_patterns, regex_patterns)

        block_translated = 0
        block_skipped = 0
        block_ignored = 0
        block_transferred = 0

        # Übersetze Text-Attribute in Blöcken
        for entity in msp.query('INSERT'):
            translated, skipped, ignored, transferred = translate_block_attribute(
                entity,
                doc,
                translations,
                wildcard_patterns,
                regex_patterns,
                required_attributes,
                text_source_attr,
                text_target_attr,
                target_layer,
                transfer_terms,
            )
            block_translated += translated
            block_skipped += skipped
            block_ignored += ignored
            block_transferred += transferred

        # Speichere neue DXF
        doc.saveas(output_dxf)

        translated_count = text_translated + mtext_translated + block_translated
        skipped_count = text_skipped + mtext_skipped + block_skipped
        ignored_count = text_ignored + mtext_ignored + block_ignored

        print(f"\nDXF-Übersetzung abgeschlossen:")
        print(f"  TEXT übersetzt: {text_translated}, übersprungen: {text_skipped}, ignoriert: {text_ignored}")
        print(f"  MTEXT übersetzt: {mtext_translated}, übersprungen: {mtext_skipped}, ignoriert: {mtext_ignored}")
        print(f"  {text_target_attr} (Blöcke) erstellt/aktualisiert: {block_translated}, übersprungen: {block_skipped}, ignoriert: {block_ignored}, übernommen (transfer): {block_transferred}")
        print(f"  Gesamt: {translated_count} übersetzt, {block_transferred} übernommen, {skipped_count} übersprungen, {ignored_count} ignoriert")
        print(f"  Hinweis: Übersetzungen in Blöcken wurden in {text_target_attr} geschrieben, {text_source_attr} bleibt unverändert")
        print(f"  Ausgabe-Datei: {output_dxf}")

    except Exception as e:
        print(f"Fehler beim Übersetzen der DXF-Datei: {e}")
        sys.exit(1)


def write_texts_to_excel(texts: list[str], output_path: Path,
                         translation_dicts: tuple[dict[str, str], dict[str, str], dict[str, str], dict[str, str]] = None):
    """
    Schreibt die Texte in die erste Spalte einer Excel-Datei.
    Optional: Schreibt Übersetzungen in die zweite Spalte.

    Args:
        texts: Liste der zu schreibenden Texte
        output_path: Pfad zur Ausgabe-Excel-Datei
        translation_dicts: Optional: Tuple mit (Multi, Trigramme, Bigramme, Single) für Übersetzungen
    """
    wb = Workbook()
    ws = wb.active
    ws.title = "Texte"

    # Kopfzeile
    ws['A1'] = "Text"
    if translation_dicts:
        ws['B1'] = "Übersetzung"

    # Texte schreiben
    for idx, text in enumerate(texts, start=2):
        ws[f'A{idx}'] = text

        # Optional: Übersetzung in zweite Spalte
        if translation_dicts:
            multi, trigramme, bigramme, single = translation_dicts
            translated = translate_text(text, multi, trigramme, bigramme, single)
            if translated:  # Nur wenn Übersetzung gefunden wurde
                ws[f'B{idx}'] = translated
            # Sonst bleibt die Zelle leer

    # Spaltenbreiten anpassen
    ws.column_dimensions['A'].width = 50
    if translation_dicts:
        ws.column_dimensions['B'].width = 50

    try:
        wb.save(output_path)
        print(f"Excel-Datei erfolgreich erstellt: {output_path}")
        if translation_dicts:
            print(f"  Mit Übersetzungen in Spalte B")
    except Exception as e:
        print(f"Fehler beim Schreiben der Excel-Datei: {e}")
        sys.exit(1)


def _load_existing_lang_blocks(output_path: Path, translation_languages: list[str]) -> dict:
    """
    Lädt vorhandene Sprachblöcke aus einer bestehenden JSON-Datei für Merge-Unterstützung.
    Gibt ein leeres Dict zurück wenn die Datei nicht existiert oder nicht gelesen werden kann.

    Args:
        output_path: Pfad zur bestehenden JSON-Datei
        translation_languages: Sprachen die neu generiert werden (werden aus dem Merge ausgeschlossen)

    Returns:
        Dict mit bestehenden Sprachblöcken die NICHT neu generiert werden
    """
    if not output_path.exists():
        return {}

    try:
        with open(output_path, 'r', encoding='utf-8') as f:
            existing = json.load(f)
        if not isinstance(existing, dict):
            return {}

        existing_lang_blocks = {
            key: value for key, value in existing.items()
            if key.startswith('[') and key.endswith(']') and len(key) > 2
        }

        if existing_lang_blocks:
            preserved = [k for k in existing_lang_blocks
                         if k not in {f"[{lang}]" for lang in translation_languages}]
            if preserved:
                print(f"  Bestehende Sprachblöcke übernommen: {', '.join(preserved)}")

        new_lang_keys = {f"[{lang}]" for lang in translation_languages}
        return {key: value for key, value in existing_lang_blocks.items()
                if key not in new_lang_keys}

    except Exception:
        return {}  # Fehler beim Laden → einfach neu schreiben


def _build_lang_block(
        texts: list[str],
        multi: dict[str, str],
        trigramme: dict[str, str],
        bigramme: dict[str, str],
        single: dict[str, str]) -> dict:
    """
    Übersetzt alle Texte für eine Sprache und baut den Sprach-Block für die JSON-Ausgabe.

    Args:
        texts: Liste der zu übersetzenden Texte
        multi: Multi-Phrasen-Dict
        trigramme: Trigramme-Dict
        bigramme: Bigramme-Dict
        single: Einzelwörter-Dict

    Returns:
        Dict mit {"translations": [...], "untranslated": [...]}
    """
    translations = []
    untranslated = []

    for text in texts:
        translated = translate_text(text, multi, trigramme, bigramme, single)
        if translated:
            translations.append({"source": text, "target": translated})
        else:
            untranslated.append({"source": text, "target": ""})

    return {"translations": translations, "untranslated": untranslated}


def write_texts_to_json(texts: list[str], ignored_texts: list[str], output_path: Path,
                        translation_dicts: tuple[dict[str, str], dict[str, str], dict[str, str], dict[str, str]] = None,
                        translation_dicts_all: dict[str, tuple] = None,
                        translation_languages: list[str] = None):
    """
    Schreibt die Texte in eine JSON-Datei.

    Immer wenn Übersetzungssprachen angegeben sind (auch nur eine), wird die
    hierarchische Struktur verwendet:
    - "[CS]": {"translations": [...], "untranslated": [...]}
    - "[EN]": {"translations": [...], "untranslated": [...]}
    - "ignored": Liste von Strings für gefilterte/ignorierte Texte

    Existiert die Ausgabedatei bereits und enthält sie Sprachblöcke, werden
    diese ERHALTEN und nur die neu angegebenen Sprachen aktualisiert. So können
    CS- und EN-Übersetzungen schrittweise in dieselbe Datei geschrieben werden.

    Nur ohne --translate (kein translation_languages) wird die flache Legacy-Struktur
    erzeugt – sie dient als reiner Rohextrakt ohne Sprachzuordnung:
    - "translations": []  (immer leer, da keine Sprache bekannt)
    - "untranslated": [...]
    - "ignored": [...]
    Diese Dateien können anschliessend mit --retranslate-json + --translate LANG
    in das Multilang-Format konvertiert und übersetzt werden.

    Args:
        texts: Liste der akzeptierten Texte
        ignored_texts: Liste der ignorierten Texte
        output_path: Pfad zur Ausgabe-JSON-Datei
        translation_dicts: Optional: Tuple für eine Sprache (für Excel/Text-Ausgabe, hier ignoriert)
        translation_dicts_all: Optional: Dict mit {LANG: (multi, trigramme, bigramme, single)}
        translation_languages: Optional: Liste der Sprachen in der Reihenfolge der Verarbeitung
    """
    output_data = {}

    # Hierarchische Struktur: immer wenn mindestens eine Übersetzungssprache angegeben
    if translation_dicts_all and translation_languages:

        # Lade bestehende Sprachblöcke aus vorhandener Datei (Merge-Unterstützung)
        output_data.update(_load_existing_lang_blocks(output_path, translation_languages))

        # Generiere neue/aktualisierte Sprachblöcke
        for lang in translation_languages:
            if lang not in translation_dicts_all:
                continue
            multi, trigramme, bigramme, single = translation_dicts_all[lang]
            output_data[f"[{lang}]"] = _build_lang_block(texts, multi, trigramme, bigramme, single)

        # Ignorierte Texte am Ende (ohne Sprachzuordnung)
        output_data["ignored"] = ignored_texts

    # Flache Struktur: ohne Übersetzungssprachen
    else:
        if translation_dicts:
            multi, trigramme, bigramme, single = translation_dicts
            lang_block = _build_lang_block(texts, multi, trigramme, bigramme, single)
            output_data["translations"] = lang_block["translations"]
            output_data["untranslated"] = lang_block["untranslated"]
        else:
            # Ohne Übersetzungs-Config: alle akzeptierten Texte sind nicht übersetzt
            output_data["translations"] = []
            output_data["untranslated"] = [{"source": text, "target": ""} for text in texts]

        output_data["ignored"] = ignored_texts

    try:
        with open(output_path, 'w', encoding='utf-8') as f:
            json.dump(output_data, f, ensure_ascii=False, indent=2)

        print(f"JSON-Datei erfolgreich erstellt: {output_path}")

        # Ausgabe-Statistiken
        if translation_dicts_all and translation_languages:
            # Alle Sprachblöcke in der Ausgabedatei anzeigen (inkl. übernommene)
            for key in output_data:
                if key.startswith('[') and key.endswith(']') and len(key) > 2:
                    lang_data = output_data[key]
                    lang = key[1:-1]
                    marker = "" if lang in translation_languages else " (übernommen)"
                    print(f"  [{lang}]{marker} Übersetzungen: {len(lang_data['translations'])}, "
                          f"Nicht übersetzt: {len(lang_data['untranslated'])}")
        else:
            if translation_dicts:
                print(f"  Übersetzungen: {len(output_data['translations'])}")
                print(f"  Nicht übersetzte Texte: {len(output_data['untranslated'])}")
            else:
                print(f"  Nicht übersetzte Texte: {len(output_data['untranslated'])}")

        print(f"  Ignorierte Texte: {len(ignored_texts)}")
    except Exception as e:
        print(f"Fehler beim Schreiben der JSON-Datei: {e}")
        sys.exit(1)


def write_texts_to_text(texts: list[str], ignored_texts: list[str], output_path: Path,
                        translation_dicts: tuple[dict[str, str], dict[str, str], dict[str, str], dict[str, str]] = None):
    """
    Schreibt die Texte in eine Textdatei mit drei Abschnitten.

    Format:
    --- translations:

    source1 -> target1
    source2 -> target2

    --- untranslated:

    text1
    text2

    --- ignored:

    ignored1
    ignored2

    Args:
        texts: Liste der akzeptierten Texte
        ignored_texts: Liste der ignorierten Texte
        output_path: Pfad zur Ausgabe-Textdatei
        translation_dicts: Optional: Tuple mit (Multi, Trigramme, Bigramme, Single) für Übersetzungen
    """
    try:
        with open(output_path, 'w', encoding='utf-8') as f:
            # Block 1: Übersetzte Texte
            if translation_dicts:
                multi, trigramme, bigramme, single = translation_dicts
                translations = []
                untranslated = []

                for text in texts:
                    translated = translate_text(text, multi, trigramme, bigramme, single)
                    if translated:
                        translations.append((text, translated))
                    else:
                        untranslated.append(text)

                # Schreibe translations Block
                f.write("--- translations:\n\n")
                for source, target in translations:
                    f.write(f"{source} -> {target}\n")

                # Schreibe untranslated Block
                f.write("\n--- untranslated:\n\n")
                for text in untranslated:
                    f.write(f"{text}\n")

            else:
                # Ohne Übersetzungs-Config: alle Texte sind nicht übersetzt
                f.write("--- translations:\n\n")
                f.write("\n--- untranslated:\n\n")
                for text in texts:
                    f.write(f"{text}\n")

            # Block 3: Ignorierte Texte
            f.write("\n--- ignored:\n\n")
            for text in ignored_texts:
                f.write(f"{text}\n")

        print(f"Text-Datei erfolgreich erstellt: {output_path}")
        if translation_dicts:
            translations_count = len(translations)
            untranslated_count = len(texts) - translations_count
            print(f"  Übersetzungen: {translations_count}")
            print(f"  Nicht übersetzte Texte: {untranslated_count}")
        else:
            print(f"  Nicht übersetzte Texte: {len(texts)}")
        print(f"  Ignorierte Texte: {len(ignored_texts)}")

    except Exception as e:
        print(f"Fehler beim Schreiben der Text-Datei: {e}")
        sys.exit(1)


def _dedup_source_list(
        entry_list: list,
        seen_from_other: set[str],
        label: str,
        list_name: str,
        other_name: str) -> tuple[list, set[str]]:
    """
    Bereinigt eine Liste von {source, target}-Einträgen um Duplikate.
    Das erste Vorkommen je source wird behalten.

    Zwei Fälle werden gemeldet:
    - source bereits in seen_from_other → Cross-Duplikat mit der anderen Liste
    - source bereits in dieser Liste → internes Duplikat

    Args:
        entry_list:      Zu prüfende Liste
        seen_from_other: Sources aus der Gegenliste (für Cross-Duplikat-Check)
        label:           Block-Bezeichnung für Warnmeldungen, z.B. "[CS]"
        list_name:       Name dieser Liste, z.B. "'translations'"
        other_name:      Name der Gegenliste, z.B. "'untranslated'"

    Returns:
        Tuple (bereinigte Liste, set der in dieser Liste gesehenen sources)
    """
    seen: set[str] = set()
    result = []
    for entry in entry_list:
        if not isinstance(entry, dict):
            continue
        source = entry.get('source', '')
        if source in seen_from_other:
            print(f"  WARNUNG: {label} '{source}' aus {list_name} entfernt "
                  f"(bereits in {other_name})")
        elif source in seen:
            print(f"  WARNUNG: {label} Duplikat in {list_name} entfernt: '{source}'")
        else:
            seen.add(source)
            result.append(entry)
    return result, seen


def _retranslate_language_block(
        translations_list: list,
        untranslated_list: list,
        multi: dict[str, str],
        trigramme: dict[str, str],
        bigramme: dict[str, str],
        single: dict[str, str],
        label: str,
        transfer_terms: set[str] | None = None) -> tuple[int, int, int, int]:
    """
    Verarbeitet einen einzelnen Sprach-Block einer JSON-Übersetzungsdatei in-place.

    Schritt 0 – Deduplizierung beider Listen (via _dedup_source_list):
      Doppelte source-Einträge werden entfernt (erstes Vorkommen wird behalten).
      Einträge, deren source bereits in translations_list vorkommt, werden aus
      untranslated_list entfernt.

    Schritt 1 – Re-übersetze bestehende Einträge in translations_list:
      Jeder Eintrag wird mit der aktuellen Config neu übersetzt.
      Wird keine Übersetzung mehr gefunden, wandert der Eintrag nach untranslated_list.

    Schritt 2 – Auto-übersetze Einträge aus untranslated_list:
      Für jeden Eintrag mit leerem target wird auto-übersetzt;
      erfolgreiche Treffer wandern nach translations_list.
      Einträge mit bereits gesetztem target (manuell befüllt) werden
      ebenfalls direkt nach translations_list übernommen.

    Schritt 3 – Transfer-Begriffe werden 1:1 übernommen (ohne Übersetzung).

    Args:
        translations_list: Liste der bereits übersetzten Einträge (in-place verändert)
        untranslated_list: Liste der noch nicht übersetzten Einträge (in-place verändert)
        multi: Multi-Phrasen-Dict
        trigramme: Trigramme-Dict
        bigramme: Bigramme-Dict
        single: Einzelwörter-Dict
        label: Bezeichnung für Warnmeldungen, z.B. "[CS]"
        transfer_terms: Set mit Begriffen die 1:1 übernommen werden (aus [transfer] Sektion)

    Returns:
        Tuple (aktualisiert, unveraendert, neu_aus_untranslated, transferred)
    """
    updated = 0
    unchanged = 0
    newly = 0
    transferred = 0

    # Schritt 0: Deduplizierung – erstes Vorkommen je source wird behalten
    deduped_t, seen_t = _dedup_source_list(
        translations_list, set(), label, "'translations'", "'untranslated'")
    translations_list[:] = deduped_t

    deduped_u, _ = _dedup_source_list(
        untranslated_list, seen_t, label, "'untranslated'", "'translations'")
    untranslated_list[:] = deduped_u

    # Schritt 1: Re-übersetze bestehende translations-Einträge
    for entry in translations_list[:]:  # Kopie für sicheres Entfernen
        if not isinstance(entry, dict) or 'source' not in entry:
            continue
        source = entry['source']
        old_target = entry.get('target', '')
        new_target = translate_text(source, multi, trigramme, bigramme, single, transfer_terms)
        if new_target:
            if new_target != old_target:
                entry['target'] = new_target
                updated += 1
            else:
                unchanged += 1
        else:
            # Übersetzung nicht mehr verfügbar → nach untranslated verschieben
            untranslated_list.append({"source": source, "target": ""})
            translations_list.remove(entry)
            print(f"  WARNUNG: {label} Übersetzung für '{source}' nicht mehr verfügbar, "
                  f"verschoben zu 'untranslated'")

    # Schritt 2: Auto-übersetze Einträge aus untranslated_list
    for entry in untranslated_list[:]:  # Kopie für sicheres Entfernen
        if not isinstance(entry, dict) or 'source' not in entry:
            continue
        if entry.get('target', ''):
            # Manuell befüllter Eintrag → direkt nach translations verschieben
            translations_list.append(entry)
            untranslated_list.remove(entry)
            newly += 1
            continue
        source = entry['source']
        new_target = translate_text(source, multi, trigramme, bigramme, single, transfer_terms)
        if new_target:
            translations_list.append({"source": source, "target": new_target})
            untranslated_list.remove(entry)
            # Prüfe ob es ein Transfer-Begriff war
            if transfer_terms and source in transfer_terms:
                transferred += 1
            else:
                newly += 1

    return updated, unchanged, newly, transferred


def _retranslate_legacy(data: dict,
                        translation_languages: list[str],
                        translation_dicts_all: dict[str, tuple],
                        transfer_terms_all: dict[str, set[str]] | None = None) -> dict:
    """
    Verarbeitet eine Legacy-Flat-JSON-Struktur und konvertiert sie ins Multilang-Format.

    Args:
        data: Geladene JSON-Daten (flache Struktur mit 'translations'/'untranslated')
        translation_languages: Sprachen in der Reihenfolge der Verarbeitung
        translation_dicts_all: Dict mit {LANG: (multi, trigramme, bigramme, single), ...}
        transfer_terms_all: Dict mit {LANG: set[transfer_terms], ...} (aus [transfer] Sektion)

    Returns:
        Konvertierte Daten im Multilang-Format
    """
    if 'translations' not in data or not isinstance(data['translations'], list):
        print(f"FEHLER: JSON-Datei enthält keinen gültigen 'translations' Block")
        sys.exit(1)
    if not translation_languages or translation_languages[0] not in translation_dicts_all:
        print(f"FEHLER: Keine Übersetzungs-Config für Re-Übersetzung verfügbar")
        sys.exit(1)

    if 'untranslated' not in data:
        data['untranslated'] = []

    lang = translation_languages[0]
    multi, trigramme, bigramme, single = translation_dicts_all[lang]
    transfer_terms = transfer_terms_all.get(lang, set()) if transfer_terms_all else set()

    updated, unchanged, newly, transferred = _retranslate_language_block(
        data['translations'], data['untranslated'],
        multi, trigramme, bigramme, single,
        f"[{lang}]",
        transfer_terms
    )

    print(f"\nRe-Übersetzung abgeschlossen (alte Struktur → wird zu multilang konvertiert, Sprache: {lang}):")
    print(f"  Aktualisiert (translations): {updated}")
    print(f"  Unverändert: {unchanged}")
    if newly:
        print(f"  Neu aus 'untranslated' übersetzt/verschoben: {newly}")
    if transferred:
        print(f"  Transfer-Begriffe (1:1): {transferred}")

    # Legacy → Multilang konvertieren: flache Struktur in [LANG]-Block einpacken
    return {
        f"[{lang}]": {
            "translations": data['translations'],
            "untranslated": data['untranslated'],
        },
        "ignored": data.get('ignored', []),
    }


def _retranslate_multilang(data: dict,
                           existing_languages: list[str],
                           translation_languages: list[str],
                           translation_dicts_all: dict[str, tuple],
                           transfer_terms_all: dict[str, set[str]] | None = None) -> None:
    """
    Verarbeitet eine Multilang-JSON-Struktur in-place.

    Args:
        data: Geladene JSON-Daten (hierarchische Multilang-Struktur, wird in-place verändert)
        existing_languages: Sprachen die bereits in der Datei vorhanden sind
        translation_languages: Sprachen in der Reihenfolge der Verarbeitung
        translation_dicts_all: Dict mit {LANG: (multi, trigramme, bigramme, single), ...}
        transfer_terms_all: Dict mit {LANG: set[transfer_terms], ...} (aus [transfer] Sektion)
    """
    print(f"Erkannte mehrsprachige Struktur mit Sprachen: {', '.join(existing_languages)}")

    total_updated = 0
    total_unchanged = 0
    total_newly = 0
    total_transferred = 0
    new_block_count = 0

    for lang in translation_languages:
        lang_key = f"[{lang}]"
        if lang_key not in data:
            print(f"  Erstelle neuen Sprach-Block: [{lang}]")
            data[lang_key] = {"translations": [], "untranslated": []}
            new_block_count += 1

        lang_data = data[lang_key]
        if not isinstance(lang_data, dict):
            print(f"WARNUNG: [{lang}] Block ist kein Object, überspringe")
            continue
        if 'translations' not in lang_data:
            lang_data['translations'] = []
        if 'untranslated' not in lang_data:
            lang_data['untranslated'] = []
        if not isinstance(lang_data['translations'], list):
            print(f"WARNUNG: [{lang}][translations] ist kein Array, überspringe")
            continue
        if lang not in translation_dicts_all:
            print(f"WARNUNG: Keine Übersetzungs-Config für [{lang}] verfügbar, überspringe")
            continue

        multi, trigramme, bigramme, single = translation_dicts_all[lang]
        transfer_terms = transfer_terms_all.get(lang, set()) if transfer_terms_all else set()
        updated, unchanged, newly, transferred = _retranslate_language_block(
            lang_data['translations'], lang_data['untranslated'],
            multi, trigramme, bigramme, single,
            f"[{lang}]",
            transfer_terms
        )
        total_updated += updated + newly
        total_unchanged += unchanged
        total_newly += newly
        total_transferred += transferred

        print(f"  [{lang}] Aktualisiert: {updated}, Unverändert: {unchanged}", end="")
        if newly:
            print(f", Neu aus 'untranslated': {newly}", end="")
        if transferred:
            print(f", Transfer-Begriffe (1:1): {transferred}", end="")
        print()

    print(f"\nRe-Übersetzung abgeschlossen (mehrsprachige Struktur):")
    print(f"  Gesamt aktualisiert: {total_updated}")
    print(f"  Gesamt unverändert: {total_unchanged}")
    if total_transferred:
        print(f"  Gesamt Transfer-Begriffe (1:1): {total_transferred}")
    if new_block_count:
        print(f"  Neue Sprach-Blöcke erstellt: {new_block_count}")


def retranslate_json_file(json_path: Path,
                          translation_dicts_all: dict[str, tuple],
                          translation_languages: list[str],
                          transfer_terms_all: dict[str, set[str]] | None = None):
    """
    Re-übersetzt eine JSON-Datei mit neuen Übersetzungs-Configs.
    Überschreibt die ursprüngliche JSON-Datei mit den aktualisierten Übersetzungen.

    Erkennt die Dateistruktur automatisch und delegiert die eigentliche
    Übersetzungsarbeit an _retranslate_language_block():
    - Alte Struktur:         "translations"/  "untranslated" auf oberster Ebene
    - Hierarchische Struktur: "[CS]": {"translations": ..., "untranslated": ...}

    Args:
        json_path: Pfad zur JSON-Datei
        translation_dicts_all: Dict mit {LANG: (multi, trigramme, bigramme, single), ...}
        translation_languages: Liste der Sprachen in der Reihenfolge der Verarbeitung
    """
    try:
        with open(json_path, 'r', encoding='utf-8') as f:
            data = json.load(f)

        if not isinstance(data, dict):
            print(f"FEHLER: JSON-Datei hat ungültige Struktur (erwartet Object, "
                  f"bekam {type(data).__name__})")
            sys.exit(1)

        structure_type, existing_languages = detect_json_structure(data)

        if structure_type == "legacy":
            data = _retranslate_legacy(data, translation_languages, translation_dicts_all, transfer_terms_all)
        elif structure_type == "multilang":
            _retranslate_multilang(data, existing_languages, translation_languages, translation_dicts_all, transfer_terms_all)
        else:
            print(f"FEHLER: JSON-Datei hat unbekannte Struktur")
            sys.exit(1)

        with open(json_path, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)

        print(f"\nJSON-Datei erfolgreich aktualisiert: {json_path}")

    except json.JSONDecodeError as e:
        print(f"Fehler beim Parsen der JSON-Datei: {e}")
        sys.exit(1)
    except Exception as e:
        print(f"Fehler beim Re-Übersetzen der JSON-Datei: {e}")
        sys.exit(1)


def resolve_input_file(name: str, work_dir: Path, label: str = "Datei") -> Path:
    """
    Sucht eine Datei zuerst im Work-Ordner, dann als vollständigen/relativen Pfad.
    Gibt den aufgelösten Path zurück oder beendet das Programm mit Fehlermeldung.

    Verwendet intern check_file_in_work() aus utils für die Dateisuche.

    Args:
        name:     Dateiname oder Pfad (wie vom Benutzer angegeben)
        work_dir: Arbeitsverzeichnis, in dem zuerst gesucht wird
        label:    Bezeichnung der Datei für die Fehlermeldung (z.B. "DXF-Datei")

    Returns:
        Path zur gefundenen Datei
    """
    path, found = check_file_in_work(work_dir, name)
    if not found:
        print(f"{label} nicht gefunden: {name}")
        print(f"  Geprüft in Work-Ordner: {work_dir / name}")
        print(f"  Geprüft als vollständiger Pfad: {Path(name)}")
        sys.exit(1)
    return path


def _parse_txt_sections(filename: Path) -> tuple[list[tuple[str, str]], list[str], list[str]]:
    """
    Parst eine Textdatei im Format von write_texts_to_text.

    Erkannte Abschnitte:
    - "--- translations:" → Zeilen im Format "source -> target"
    - "--- untranslated:"  → einfache Texte (eine pro Zeile)
    - "--- ignored:"       → einfache Texte (eine pro Zeile)

    Args:
        filename: Pfad zur Textdatei

    Returns:
        Tuple (translations, untranslated, ignored):
        - translations: Liste von (source, target) Tupeln
        - untranslated: Liste von source-Texten ohne Übersetzung
        - ignored:      Liste von ignorierten Texten
    """
    translations: list[tuple[str, str]] = []
    untranslated: list[str] = []
    ignored: list[str] = []
    current_section: str | None = None

    try:
        with open(filename, 'r', encoding='utf-8') as f:
            for line in f:
                stripped = line.rstrip('\n\r').strip()

                if stripped.startswith('---'):
                    low = stripped.lower()
                    if 'translations' in low:
                        current_section = 'translations'
                    elif 'untranslated' in low:
                        current_section = 'untranslated'
                    elif 'ignored' in low:
                        current_section = 'ignored'
                    else:
                        current_section = None
                    continue

                if not stripped or current_section is None:
                    continue

                if current_section == 'translations':
                    if ' -> ' in stripped:
                        source, target = stripped.split(' -> ', 1)
                        translations.append((source, target))
                    else:
                        # Eintrag ohne Ziel → als untranslated behandeln
                        untranslated.append(stripped)
                elif current_section == 'untranslated':
                    untranslated.append(stripped)
                elif current_section == 'ignored':
                    ignored.append(stripped)

    except Exception as e:
        print(f"Fehler beim Lesen der Textdatei: {e}")
        sys.exit(1)

    return translations, untranslated, ignored


def retranslate_txt_file(txt_path: Path,
                         translation_dicts_all: dict[str, tuple],
                         translation_languages: list[str],
                         wildcard_patterns: list[str] | None = None,
                         regex_patterns: list[str] | None = None,
                         transfer_terms_all: dict[str, set[str]] | None = None) -> None:
    """
    Re-übersetzt eine Textdatei im Format von write_texts_to_text.
    Überschreibt die ursprüngliche Datei mit aktualisierten Inhalten.

    Verarbeitungsreihenfolge:
    1. Bestehende translations werden mit aktueller Config neu übersetzt.
       Texte für die keine Übersetzung mehr gefunden wird, wandern nach untranslated.
    2. Einträge aus untranslated werden gegen die Ignore-Pattern geprüft:
       - Treffer → ignored (neu erkannte Pattern seit dem letzten Extrakt)
       - Kein Treffer → auto-übersetzen; Erfolg → translations, sonst bleibt in untranslated.
    3. Transfer-Begriffe werden 1:1 übernommen (ohne Übersetzung).

    Arbeitet immer mit der ersten Sprache aus translation_languages.

    Args:
        txt_path: Pfad zur Textdatei (wird in-place überschrieben)
        translation_dicts_all: Dict mit {LANG: (multi, trigramme, bigramme, single), ...}
        translation_languages: Liste der Sprachen (nur die erste wird verwendet)
        wildcard_patterns: Wildcard-Muster zum Filtern (aus translator.cfg)
        regex_patterns: Regex-Muster zum Filtern (aus translator.cfg)
        transfer_terms_all: Dict mit {LANG: set[transfer_terms], ...} (aus [transfer] Sektion)
    """
    if not translation_languages or translation_languages[0] not in translation_dicts_all:
        print(f"FEHLER: Keine Übersetzungs-Config für Re-Übersetzung verfügbar")
        sys.exit(1)

    wildcard_patterns = wildcard_patterns or []
    regex_patterns = regex_patterns or []

    lang = translation_languages[0]
    multi, trigramme, bigramme, single = translation_dicts_all[lang]
    transfer_terms = transfer_terms_all.get(lang, set()) if transfer_terms_all else set()

    existing_translations, untranslated, ignored = _parse_txt_sections(txt_path)

    print(f"  Vorhandene Übersetzungen: {len(existing_translations)}")
    print(f"  Nicht übersetzt:          {len(untranslated)}")
    print(f"  Ignoriert:                {len(ignored)}")

    new_translations: list[tuple[str, str]] = []
    updated = 0
    unchanged = 0
    moved_to_untranslated: list[str] = []

    # Schritt 1: Bestehende Übersetzungen neu übersetzen
    for source, old_target in existing_translations:
        new_target = translate_text(source, multi, trigramme, bigramme, single, transfer_terms)
        if new_target:
            new_translations.append((source, new_target))
            if new_target != old_target:
                updated += 1
            else:
                unchanged += 1
        else:
            moved_to_untranslated.append(source)
            print(f"  WARNUNG: Übersetzung nicht mehr verfügbar für '{source}', "
                  f"verschoben zu 'untranslated'")

    # Schritt 2: Untranslated Texte prüfen und auto-übersetzen
    still_untranslated: list[str] = list(moved_to_untranslated)
    newly = 0
    newly_ignored = 0
    transferred = 0
    for text in untranslated:
        # Neu hinzugefügte Ignore-Pattern prüfen
        if should_ignore_text(text, wildcard_patterns, regex_patterns):
            ignored.append(text)
            newly_ignored += 1
            continue
        new_target = translate_text(text, multi, trigramme, bigramme, single, transfer_terms)
        if new_target:
            new_translations.append((text, new_target))
            # Prüfe ob es ein Transfer-Begriff war
            if transfer_terms and text in transfer_terms:
                transferred += 1
            else:
                newly += 1
        else:
            still_untranslated.append(text)

    # Ergebnis zurückschreiben
    try:
        with open(txt_path, 'w', encoding='utf-8') as f:
            f.write("--- translations:\n\n")
            for source, target in new_translations:
                f.write(f"{source} -> {target}\n")
            f.write("\n--- untranslated:\n\n")
            for text in still_untranslated:
                f.write(f"{text}\n")
            f.write("\n--- ignored:\n\n")
            for text in ignored:
                f.write(f"{text}\n")
    except Exception as e:
        print(f"Fehler beim Schreiben der Textdatei: {e}")
        sys.exit(1)

    print(f"\nRe-Übersetzung abgeschlossen (Sprache: {lang}):")
    print(f"  Aktualisiert:  {updated}")
    print(f"  Unverändert:   {unchanged}")
    if newly:
        print(f"  Neu übersetzt: {newly}")
    if transferred:
        print(f"  Transfer-Begriffe (1:1): {transferred}")
    if newly_ignored:
        print(f"  Neu ignoriert (Pattern): {newly_ignored}")
    if moved_to_untranslated:
        print(f"  Zurück zu untranslated: {len(moved_to_untranslated)}")
    print(f"  Noch nicht übersetzt: {len(still_untranslated)}")
    print(f"\nDatei aktualisiert: {txt_path}")


def _run_retranslate_txt(
        args,
        work_dir: Path,
        translation_dicts_all: dict,
        translation_languages: list,
        wildcard_patterns: list,
        regex_patterns: list,
        transfer_terms_all: dict) -> None:
    """Workflow: Re-Übersetzung einer bestehenden TXT-Datei (--retranslate-txt)."""
    print("=== Re-Übersetzung TXT-Datei ===\n")

    input_filename = args.retranslate_txt
    if not input_filename.lower().endswith('.txt'):
        print("Mit --retranslate-txt nur .txt Dateien erlaubt")
        sys.exit(1)

    txt_file = resolve_input_file(input_filename, work_dir, "TXT-Datei")

    print(f"TXT-Datei: {txt_file}")
    print(f"Sprache:   {translation_languages[0]}")

    retranslate_txt_file(txt_file, translation_dicts_all, translation_languages,
                         wildcard_patterns, regex_patterns, transfer_terms_all)

    print("\nVerarbeitung erfolgreich abgeschlossen.")
    sys.exit(0)


def _build_arg_parser() -> argparse.ArgumentParser:
    """Erstellt und konfiguriert den Argument-Parser für translate.py."""
    parser = argparse.ArgumentParser(
        description='Extrahiert TEXT/MTEXT aus DXF, filtert Texte aus TXT, verarbeitet JSON, oder übersetzt DXF direkt. Optional mit Übersetzungsfunktion.',
        prog='translate'
    )

    # Mutually exclusive group: entweder DXF-Extraktion, TXT-Filterung, JSON-Reprocessing oder DXF-zu-DXF
    input_group = parser.add_mutually_exclusive_group(required=True)
    input_group.add_argument(
        '-e', '--extract',
        action='store_true',
        help='Extrahiert Texte aus DXF/DWG Datei (benötigt --filename)'
    )
    input_group.add_argument(
        '--fromtxt',
        action='store',
        metavar='FILE.txt',
        help='Filtert Texte aus einer TXT Datei (ein Text pro Zeile)'
    )
    input_group.add_argument(
        '--fromjson',
        action='store',
        metavar='FILE.json',
        help='Liest untranslated Block aus JSON-Datei und übersetzt erneut (benötigt --translate)'
    )
    input_group.add_argument(
        '--retranslate-json',
        action='store',
        metavar='FILE.json',
        help='Re-übersetzt translations Block in JSON-Datei mit aktueller Sprachkonfig (benötigt --translate)'
    )
    input_group.add_argument(
        '--retranslate-txt',
        action='store',
        metavar='FILE.txt',
        help='Re-übersetzt translations/untranslated Blöcke in TXT-Datei mit aktueller Sprachkonfig (benötigt --translate)'
    )
    input_group.add_argument(
        '--todxf',
        action='store',
        metavar='OUTPUT.dxf',
        help='Übersetzt TEXT/MTEXT in DXF und schreibt neue DXF-Datei (benötigt --filename)'
    )

    parser.add_argument(
        '-f', '--filename',
        action='store',
        help='DXF/DWG Datei (.dxf, .dwg) die verarbeitet werden soll (nur mit --extract)',
        metavar='myfile.dxf'
    )
    parser.add_argument(
        '-o', '--outname',
        action='store',
        help='Name der Ausgabedatei (ohne Pfad, mit oder ohne .xlsx/.txt Endung)',
        metavar='output.xlsx'
    )
    parser.add_argument(
        '-d', '--debug',
        action='store_true',
        help='Aktiviert Debug-Ausgabe für Pattern-Matching'
    )
    parser.add_argument(
        '-t', '--export-type',
        action='store',
        default='json',
        help='Exportformat(e): json (Standard), excel, text oder komma-separiert (z.B. excel,json,text)'
    )
    parser.add_argument(
        '--translate',
        action='store',
        metavar='LANG',
        help='Übersetzt Texte mit Config aus translation/<LANG>.cfg (z.B. CS, EN, FR). Mehrere Sprachen kommasepariert möglich (z.B. EN,FR,CS)'
    )
    parser.add_argument(
        '--lang_source',
        action='store',
        default='DE',
        metavar='LANG',
        help='Quellsprache für Attribut in Blöcken (z.B. DE->TEXT-D, EN->TEXT-EN, CS->TEXT-CS). Standard: DE'
    )
    parser.add_argument(
        '--translation-json',
        action='store',
        metavar='FILE.json',
        help='JSON-Datei mit Übersetzungen für --todxf (Standard: <filename>_texts.json)'
    )
    return parser


def _run_retranslate(
        args,
        work_dir: Path,
        translation_dicts_all: dict,
        translation_languages: list,
        transfer_terms_all: dict) -> None:
    """Workflow: Re-Übersetzung einer bestehenden JSON-Datei (--retranslate-json)."""
    print("=== Re-Übersetzung JSON-Datei ===\n")

    input_filename = args.retranslate_json
    if not input_filename.lower().endswith('.json'):
        print("Mit --retranslate-json nur .json Dateien erlaubt")
        sys.exit(1)

    json_file = resolve_input_file(input_filename, work_dir, "JSON-Datei")

    print(f"JSON-Datei: {json_file}")
    print(f"Sprachen: {', '.join(translation_languages)}")

    retranslate_json_file(json_file, translation_dicts_all, translation_languages, transfer_terms_all)

    print("\nVerarbeitung erfolgreich abgeschlossen.")
    sys.exit(0)


def _run_todxf(
        args,
        work_dir: Path,
        translation_languages: list,
        wildcard_patterns: list,
        regex_patterns: list,
        source_attr_name: str,
        target_attr_name: str,
        target_layer_name: str | None) -> None:
    """Workflow: DXF-zu-DXF Übersetzung (--todxf)."""
    print("=== DXF-zu-DXF Übersetzung ===\n")

    input_filename = args.filename
    if not input_filename.lower().endswith(('.dxf', '.dwg')):
        print("Mit --todxf nur .dxf oder .dwg Dateien als Quelle erlaubt")
        sys.exit(1)

    source_dxf = resolve_input_file(input_filename, work_dir, "Quell-DXF")
    print(f"Quell-DXF: {source_dxf}")

    # Bestimme JSON-Datei mit Übersetzungen
    if args.translation_json:
        translation_json = resolve_input_file(args.translation_json, work_dir, "Übersetzungs-JSON")
    else:
        # Standard: <filename>_texts.json im Work-Ordner
        translation_json = work_dir / (Path(input_filename).stem + "_texts.json")
        if not translation_json.exists():
            print(f"Standard-Übersetzungs-JSON nicht gefunden: {translation_json}")
            print(f"Bitte erstellen Sie die Datei oder geben Sie eine alternative mit --translation-json an")
            sys.exit(1)

    print(f"Übersetzungs-JSON: {translation_json}")

    target_lang = translation_languages[0] if translation_languages else None
    translations = load_translations_from_json(translation_json, target_lang)

    # Transfer-Begriffe aus der Sprachkonfiguration laden (nur für Symbol-Attribute)
    transfer_terms: set[str] | None = None
    if target_lang:
        translation_dir = check_environment_var('PROJECT_TRANSLATION')
        transfer_terms = load_transfer_terms(target_lang, translation_dir)

    output_dxf_name = args.todxf
    if not output_dxf_name.lower().endswith(('.dxf', '.dwg')):
        output_dxf_name += '.dxf'
    output_dxf = work_dir / output_dxf_name

    print(f"Ziel-DXF: {output_dxf}")

    translate_dxf_texts(
        source_dxf,
        output_dxf,
        translations,
        wildcard_patterns,
        regex_patterns,
        source_attr_name,
        target_attr_name,
        target_layer_name,
        transfer_terms,
    )

    print("\nVerarbeitung erfolgreich abgeschlossen.")
    sys.exit(0)


def _run_extract_or_import(
        args,
        work_dir: Path,
        wildcard_patterns: list,
        regex_patterns: list,
        translation_dicts,
        translation_dicts_all: dict,
        translation_languages: list,
        export_types: list,
        source_attr_name: str) -> None:
    """Workflow: Texte extrahieren oder importieren (--extract, --fromtxt, --fromjson)."""
    if args.extract:
        input_filename = args.filename
        if not input_filename.lower().endswith(('.dxf', '.dwg')):
            print("Mit --extract nur .dxf oder .dwg Dateien erlaubt")
            sys.exit(1)
        filename = resolve_input_file(input_filename, work_dir, "DXF-Datei")
        print(f"Lese DXF Datei: {filename}")
        all_texts = extract_text_from_dxf(filename, source_attr_name)

    elif args.fromtxt:
        input_filename = args.fromtxt
        if not input_filename.lower().endswith('.txt'):
            print("Mit --fromtxt nur .txt Dateien erlaubt")
            sys.exit(1)
        filename = resolve_input_file(input_filename, work_dir, "Textdatei")
        print(f"Lese Textdatei: {filename}")
        all_texts = read_texts_from_file(filename)

    else:  # args.fromjson
        input_filename = args.fromjson
        if not input_filename.lower().endswith('.json'):
            print("Mit --fromjson nur .json Dateien erlaubt")
            sys.exit(1)
        filename = resolve_input_file(input_filename, work_dir, "JSON-Datei")
        print(f"Lese JSON-Datei: {filename}")
        all_texts = read_texts_from_json(filename)

    texts, ignored_texts = filter_texts(all_texts, wildcard_patterns, regex_patterns, args.debug)

    extension_map = {
        'excel': '.xlsx',
        'json': '.json',
        'text': '.txt'
    }

    print(f"\nExportiere in {len(export_types)} Format(e): {', '.join(export_types)}")

    for export_type in export_types:
        if args.outname:
            base_name = args.outname
            for ext in extension_map.values():
                if base_name.endswith(ext):
                    base_name = base_name[:-len(ext)]
                    break
            output_filename = f"{base_name}{extension_map[export_type]}"
        else:
            output_filename = f"{filename.stem}_texts{extension_map[export_type]}"

        output_path = work_dir / output_filename

        if export_type == 'excel':
            write_texts_to_excel(texts, output_path, translation_dicts)
        elif export_type == 'json':
            write_texts_to_json(texts, ignored_texts, output_path, translation_dicts, translation_dicts_all, translation_languages)
        else:  # text
            write_texts_to_text(texts, ignored_texts, output_path, translation_dicts)

        print(f"  [OK] {export_type}: {output_path}")

    print("\nVerarbeitung erfolgreich abgeschlossen.")


def main() -> None:
    """Einstiegspunkt für translate.py. Parst Argumente und delegiert an Workflow-Funktionen."""
    parser = _build_arg_parser()
    args = parser.parse_args()

    # Parse und validiere Export-Typen (komma-separierte Liste)
    valid_export_types = {'excel', 'json', 'text'}
    export_types = [t.strip() for t in args.export_type.split(',')]

    for export_type in export_types:
        if export_type not in valid_export_types:
            parser.error(f"Ungültiger Export-Typ: '{export_type}'. Erlaubt sind: {', '.join(sorted(valid_export_types))}")

    seen: set = set()
    export_types = [t for t in export_types if not (t in seen or seen.add(t))]

    # Validierung der Argument-Kombinationen
    if args.extract and not args.filename:
        parser.error("--extract benötigt --filename")
    if args.filename and not (args.extract or args.todxf):
        parser.error("--filename kann nur mit --extract oder --todxf verwendet werden")
    if args.fromjson and not args.translate:
        parser.error("--fromjson benötigt --translate (Übersetzung ist erforderlich)")
    if args.retranslate_json and not args.translate:
        parser.error("--retranslate-json benötigt --translate (Übersetzung ist erforderlich)")
    if args.retranslate_txt and not args.translate:
        parser.error("--retranslate-txt benötigt --translate (Übersetzung ist erforderlich)")
    if args.todxf and not args.filename:
        parser.error("--todxf benötigt --filename (Quell-DXF muss angegeben werden)")
    if args.translation_json and not args.todxf:
        parser.error("--translation-json kann nur mit --todxf verwendet werden")

    # Umgebungsvariablen und gemeinsame Konfiguration laden
    work_dir = check_environment_var('PROJECT_WORK')
    cfg_dir = check_environment_var('PROJECT_CFG')

    wildcard_patterns, regex_patterns = load_ignore_patterns(cfg_dir)
    symbol_attr_mapping = load_symbol_attribute_mapping(cfg_dir)
    symbol_layer_mapping = load_symbol_layer_mapping(cfg_dir)

    # Zielsprachen parsen (kommasepariert, dedupliziert)
    translation_languages = []
    if args.translate:
        seen = set()
        translation_languages = [
            lang for lang in (l.strip().upper() for l in args.translate.split(','))
            if lang and not (lang in seen or seen.add(lang))
        ]

    # Attribut- und Layer-Namen bestimmen
    source_attr_name = symbol_attr_mapping.get(args.lang_source, 'TEXT-D')
    target_attr_name = symbol_attr_mapping.get(translation_languages[0], 'TEXT-CS') if translation_languages else 'TEXT-CS'
    target_layer_name = symbol_layer_mapping.get(translation_languages[0]) if translation_languages else None

    print(f"\nVerwendete Attribut-Namen:")
    print(f"  Quell-Attribut (--lang_source {args.lang_source}): {source_attr_name}")
    if translation_languages:
        if len(translation_languages) == 1:
            print(f"  Ziel-Attribut (--translate {translation_languages[0]}): {target_attr_name}")
            if target_layer_name:
                print(f"  Ziel-Layer   (--translate {translation_languages[0]}): {target_layer_name}")
        else:
            print(f"  Ziel-Sprachen (--translate): {', '.join(translation_languages)}")

    # Übersetzungs-Configs laden: {LANG: (multi, trigramme, bigramme, single), ...}
    translation_dicts_all = {}
    transfer_terms_all = {}
    if translation_languages:
        translation_dir = check_environment_var('PROJECT_TRANSLATION')
        for lang in translation_languages:
            translation_dicts_all[lang] = load_translation_config(lang, translation_dir)
            transfer_terms_all[lang] = load_transfer_terms(lang, translation_dir)

    # Für backwards compatibility: bei genau einer Sprache flaches Dict bereitstellen
    translation_dicts = translation_dicts_all.get(translation_languages[0]) if len(translation_languages) == 1 else None

    if args.debug:
        print("DEBUG-Modus aktiviert")

    # Workflow delegieren
    if args.retranslate_json:
        _run_retranslate(args, work_dir, translation_dicts_all, translation_languages, transfer_terms_all)

    if args.retranslate_txt:
        _run_retranslate_txt(args, work_dir, translation_dicts_all, translation_languages,
                             wildcard_patterns, regex_patterns, transfer_terms_all)

    if args.todxf:
        _run_todxf(args, work_dir, translation_languages, wildcard_patterns, regex_patterns,
                   source_attr_name, target_attr_name, target_layer_name)

    _run_extract_or_import(args, work_dir, wildcard_patterns, regex_patterns,
                           translation_dicts, translation_dicts_all, translation_languages,
                           export_types, source_attr_name)


if __name__ == '__main__':
    main()
