"""Dokumentations-Generator für die Vorlagendokumentation.

Generiert die Vorlagendokumentation aus mapping_config.py unter Verwendung
der Kategorie-Definitionen, Datenmodelle und sprach-spezifischen Labels.
"""
from __future__ import annotations

import warnings

from core.doc_categories import (
    CATEGORIES,
    get_category_for_prefix,
    get_subcategory_for_prefix,
)
from core import doc_models  # noqa: F401
from core import doc_labels
from core.mapping_config import MAPPING_RULES, ALWAYS_MANUAL, AVAILABILITY_MATRIX


# ---------------------------------------------------------------------------
# Platzhalter-Beschreibungen für die Kategorie "Beschreibende Metadaten"
# Max. 120 Zeichen pro Beschreibung.
# ---------------------------------------------------------------------------

_PLACEHOLDER_DESCRIPTIONS: dict[str, dict[str, str]] = {
    # --- META_ ---
    "META_ReportName": {
        "de": "Name des Berichts",
        "en": "Report name",
    },
    "META_Kurzbeschreibung": {
        "de": "Kurze Beschreibung des Berichts (1-2 Sätze)",
        "en": "Short description of the report (1-2 sentences)",
    },
    "META_Fachbereich": {
        "de": "Zuständiger Fachbereich",
        "en": "Responsible business unit",
    },
    "META_Konzerndomäne": {
        "de": "Zugehörige Konzerndomäne",
        "en": "Associated corporate domain",
    },
    "META_ProduktivDatum": {
        "de": "Datum der Produktivsetzung",
        "en": "Production deployment date",
    },
    "META_Version": {
        "de": "Aktuelle Versionsnummer des Berichts",
        "en": "Current version number of the report",
    },
    "META_LetzteAktualisierung": {
        "de": "Datum der letzten Aktualisierung",
        "en": "Date of last update",
    },
    # --- KONTAKT_ ---
    "KONTAKT_BusinessOwner": {
        "de": "Name des Business Owners",
        "en": "Business owner name",
    },
    "KONTAKT_BusinessOwnerMail": {
        "de": "E-Mail-Adresse des Business Owners",
        "en": "Business owner email address",
    },
    "KONTAKT_TechOwner": {
        "de": "Name des technischen Verantwortlichen",
        "en": "Technical owner name",
    },
    "KONTAKT_TechOwnerMail": {
        "de": "E-Mail-Adresse des technischen Verantwortlichen",
        "en": "Technical owner email address",
    },
    "KONTAKT_DataSteward": {
        "de": "Name des Data Stewards",
        "en": "Data steward name",
    },
    "KONTAKT_DataStewardMail": {
        "de": "E-Mail-Adresse des Data Stewards",
        "en": "Data steward email address",
    },
    "KONTAKT_Betrieb": {
        "de": "Name des Betriebsverantwortlichen",
        "en": "Operations contact name",
    },
    "KONTAKT_BetriebMail": {
        "de": "E-Mail-Adresse des Betriebsverantwortlichen",
        "en": "Operations contact email address",
    },
    # --- BIZ_ ---
    "BIZ_Zielsetzung": {
        "de": "Zielsetzung und Zweck des Berichts",
        "en": "Report objectives and purpose",
    },
    "BIZ_Anwendungsfall": {
        "de": "Beschreibung des Anwendungsfalls",
        "en": "Use case description",
    },
    "BIZ_Abhängigkeiten": {
        "de": "Abhängigkeiten zu anderen Berichten oder Systemen",
        "en": "Dependencies on other reports or systems",
    },
    # --- KPI_ ---
    "KPI_Name": {
        "de": "Name der Kennzahl",
        "en": "KPI name",
    },
    "KPI_Beschreibung": {
        "de": "Beschreibung der Kennzahl",
        "en": "KPI description",
    },
    "KPI_Definition": {
        "de": "Fachliche Definition und Berechnungslogik",
        "en": "Business definition and calculation logic",
    },
    # --- GLOSSAR_ ---
    "GLOSSAR_Begriff": {
        "de": "Fachbegriff aus dem Glossar",
        "en": "Glossary term",
    },
    "GLOSSAR_Definition": {
        "de": "Definition des Fachbegriffs",
        "en": "Definition of the glossary term",
    },
    # --- VER_ ---
    "VER_Repository": {
        "de": "Link zum Versionierungs-Repository",
        "en": "Link to the version control repository",
    },
    # --- LOG_ ---
    "LOG_Version": {
        "de": "Versionsnummer des Changelog-Eintrags",
        "en": "Version number of the changelog entry",
    },
    "LOG_Datum": {
        "de": "Datum der Änderung",
        "en": "Date of the change",
    },
    "LOG_Änderung": {
        "de": "Beschreibung der durchgeführten Änderung",
        "en": "Description of the change",
    },
    "LOG_AlterWert": {
        "de": "Vorheriger Wert vor der Änderung",
        "en": "Previous value before the change",
    },
    "LOG_NeuerWert": {
        "de": "Neuer Wert nach der Änderung",
        "en": "New value after the change",
    },
    "LOG_Typ": {
        "de": "Art der Änderung (z.B. Hinzufügung, Korrektur)",
        "en": "Type of change (e.g. addition, correction)",
    },
    "LOG_Autor": {
        "de": "Autor der Änderung",
        "en": "Author of the change",
    },
    # --- REVIEW_ ---
    "REVIEW_FachName": {
        "de": "Name des fachlichen Freigebenden",
        "en": "Business approver name",
    },
    "REVIEW_FachDatum": {
        "de": "Datum der fachlichen Freigabe",
        "en": "Business approval date",
    },
    "REVIEW_FachKommentar": {
        "de": "Kommentar zur fachlichen Freigabe",
        "en": "Business approval comment",
    },
    "REVIEW_TechName": {
        "de": "Name des technischen Freigebenden",
        "en": "Technical approver name",
    },
    "REVIEW_TechDatum": {
        "de": "Datum der technischen Freigabe",
        "en": "Technical approval date",
    },
    "REVIEW_TechKommentar": {
        "de": "Kommentar zur technischen Freigabe",
        "en": "Technical approval comment",
    },
    # --- DS_ (Datenquellen) ---
    "DS_Quelle": {
        "de": "Name der Datenquelle",
        "en": "Data source name",
    },
    "DS_System": {
        "de": "Typ des Quellsystems (z.B. SQL Server, SharePoint)",
        "en": "Source system type (e.g. SQL Server, SharePoint)",
    },
    "DS_SchemaTabellen": {
        "de": "Schema und Tabellen der Datenquelle",
        "en": "Schema and tables of the data source",
    },
    "DS_Owner": {
        "de": "Verantwortlicher der Datenquelle",
        "en": "Data source owner",
    },
    "DS_Detailbeschreibung": {
        "de": "Detaillierte Beschreibung der Datenquelle",
        "en": "Detailed description of the data source",
    },
    "DS_PipelineReferenz": {
        "de": "Referenz auf zugehörige Datenpipeline",
        "en": "Reference to associated data pipeline",
    },
    # --- PQ_ (Power Query) ---
    "PQ_Name": {
        "de": "Name der Power-Query-Abfrage",
        "en": "Power Query query name",
    },
    "PQ_Quelle": {
        "de": "Quelle der Power-Query-Abfrage",
        "en": "Power Query query source",
    },
    "PQ_ZielTabelle": {
        "de": "Zieltabelle der Power-Query-Abfrage",
        "en": "Target table of the Power Query query",
    },
    "PQ_Beschreibung": {
        "de": "Beschreibung der Power-Query-Abfrage",
        "en": "Power Query query description",
    },
    "PQ_ServiceHinweis": {
        "de": "Hinweis zu Service-Abhängigkeiten der Abfrage",
        "en": "Service dependency notes for the query",
    },
    "PQ_NullHandling": {
        "de": "Behandlung von NULL-Werten in Power Query",
        "en": "NULL value handling in Power Query",
    },
    "PQ_ErrorHandling": {
        "de": "Fehlerbehandlungsstrategie in Power Query",
        "en": "Error handling strategy in Power Query",
    },
    "PQ_Validierungsregeln": {
        "de": "Validierungsregeln für Power-Query-Daten",
        "en": "Validation rules for Power Query data",
    },
    "PQ_QueryFolding": {
        "de": "Status des Query Folding (ja/nein/teilweise)",
        "en": "Query folding status (yes/no/partial)",
    },
    "PQ_PerformanceHinweise": {
        "de": "Hinweise zur Performance-Optimierung",
        "en": "Performance optimization notes",
    },
    "PQ_Optimierungen": {
        "de": "Durchgeführte Optimierungen in Power Query",
        "en": "Optimizations applied in Power Query",
    },
    # --- PQT_ (Transformationen) ---
    "PQT_Abfrage": {
        "de": "Name der Abfrage mit Transformationsschritten",
        "en": "Query name with transformation steps",
    },
    "PQT_MCode": {
        "de": "M-Code-Ausdruck der Transformation",
        "en": "M code expression of the transformation",
    },
    "PQT_Schrittname": {
        "de": "Name des Transformationsschritts",
        "en": "Transformation step name",
    },
    # --- PARAM_ (Parameter) ---
    "PARAM_Name": {
        "de": "Name des Power-Query-Parameters",
        "en": "Power Query parameter name",
    },
    "PARAM_Beschreibung": {
        "de": "Beschreibung des Parameters",
        "en": "Parameter description",
    },
    "PARAM_Standardwert": {
        "de": "Standardwert des Parameters",
        "en": "Default value of the parameter",
    },
    # --- COL_ (Spalten) ---
    "COL_Abfrage": {
        "de": "Tabellenname der Spalte",
        "en": "Table name of the column",
    },
    "COL_Spalte": {
        "de": "Name der Spalte",
        "en": "Column name",
    },
    "COL_QuellTyp": {
        "de": "Datentyp in der Quelle",
        "en": "Source data type",
    },
    "COL_ZielTyp": {
        "de": "Datentyp im Modell",
        "en": "Model data type",
    },
    # --- MODELL_ (Datenmodell) ---
    "MODELL_DatasetName": {
        "de": "Name des Datasets",
        "en": "Dataset name",
    },
    "MODELL_BerichtName": {
        "de": "Name des Berichts im Modell",
        "en": "Report name in the model",
    },
    "MODELL_AppName": {
        "de": "Name der Power BI App",
        "en": "Power BI App name",
    },
    "MODELL_CalcBeschreibung": {
        "de": "Beschreibung berechneter Tabellen/Spalten",
        "en": "Description of calculated tables/columns",
    },
    # --- TBLTYP_ (Tabellentypen) ---
    "TBLTYP_Tabelle": {
        "de": "Name der Tabelle",
        "en": "Table name",
    },
    "TBLTYP_Typ": {
        "de": "Typ der Tabelle (Import, DirectQuery, Dual, Calculated)",
        "en": "Table type (Import, DirectQuery, Dual, Calculated)",
    },
    "TBLTYP_Beschreibung": {
        "de": "Beschreibung der Tabelle",
        "en": "Table description",
    },
    # --- REL_ (Beziehungen) ---
    "REL_Von": {
        "de": "Ausgangstabelle der Beziehung",
        "en": "Relationship source table",
    },
    "REL_Nach": {
        "de": "Zieltabelle der Beziehung",
        "en": "Relationship target table",
    },
    "REL_Kardinalität": {
        "de": "Kardinalität (1:1, 1:n, n:m)",
        "en": "Cardinality (1:1, 1:n, n:m)",
    },
    "REL_Kreuzfilter": {
        "de": "Kreuzfilterrichtung (Single, Both)",
        "en": "Cross-filter direction (Single, Both)",
    },
    "REL_Aktiv": {
        "de": "Ob die Beziehung aktiv ist (ja/nein)",
        "en": "Whether the relationship is active (yes/no)",
    },
    # --- DEP_ (Abhängige Berichte) ---
    "DEP_BerichtName": {
        "de": "Name des abhängigen Berichts",
        "en": "Dependent report name",
    },
    "DEP_BerichtID": {
        "de": "ID des abhängigen Berichts",
        "en": "Dependent report ID",
    },
    "DEP_BerichtURL": {
        "de": "URL des abhängigen Berichts",
        "en": "Dependent report URL",
    },
    # --- DAX_ (Measures) ---
    "DAX_Measure": {
        "de": "Name des DAX-Measures",
        "en": "DAX measure name",
    },
    "DAX_Beschreibung": {
        "de": "Beschreibung des Measures",
        "en": "Measure description",
    },
    "DAX_Formel": {
        "de": "DAX-Formel des Measures",
        "en": "DAX formula of the measure",
    },
    # --- RLS_ (Sicherheit) ---
    "RLS_Rolle": {
        "de": "Name der RLS-Rolle",
        "en": "RLS role name",
    },
    "RLS_Beschreibung": {
        "de": "Beschreibung der RLS-Rolle",
        "en": "RLS role description",
    },
    "RLS_Tabelle": {
        "de": "Tabelle mit RLS-Filterausdruck",
        "en": "Table with RLS filter expression",
    },
    "RLS_FilterLogik": {
        "de": "DAX-Filterausdruck der RLS-Rolle",
        "en": "DAX filter expression of the RLS role",
    },
    # --- SEC_ (Sicherheit) ---
    "SEC_RLSBeschreibung": {
        "de": "Allgemeine Beschreibung der RLS-Konfiguration",
        "en": "General description of the RLS configuration",
    },
    "SEC_OLSBeschreibung": {
        "de": "Beschreibung der Object-Level Security",
        "en": "Object-Level Security description",
    },
    "SEC_Labels": {
        "de": "Vertraulichkeitslabels (Sensitivity Labels)",
        "en": "Sensitivity labels",
    },
    "SEC_Risiken": {
        "de": "Dokumentierte Sicherheitsrisiken",
        "en": "Documented security risks",
    },
    # --- REFRESH_ (Betrieb und Refresh) ---
    "REFRESH_Frequenz": {
        "de": "Aktualisierungsfrequenz (z.B. täglich, stündlich)",
        "en": "Refresh frequency (e.g. daily, hourly)",
    },
    "REFRESH_Uhrzeit": {
        "de": "Geplante Aktualisierungszeiten",
        "en": "Scheduled refresh times",
    },
    "REFRESH_Typ": {
        "de": "Art der Aktualisierung (vollständig/inkrementell)",
        "en": "Refresh type (full/incremental)",
    },
    "REFRESH_Jobs": {
        "de": "Konfigurierte Refresh-Jobs",
        "en": "Configured refresh jobs",
    },
    # --- OPS_ (Betrieb) ---
    "OPS_Monitoring": {
        "de": "Monitoring-Konfiguration und Alerts",
        "en": "Monitoring configuration and alerts",
    },
    "OPS_BekannteProbleme": {
        "de": "Bekannte Betriebsprobleme und Workarounds",
        "en": "Known operational issues and workarounds",
    },
    # --- ENDORSEMENT_ ---
    "ENDORSEMENT_Analyse": {
        "de": "Ergebnis der Endorsement-Analyse (Promoted/Certified/None)",
        "en": "Endorsement analysis result (Promoted/Certified/None)",
    },
}


# ---------------------------------------------------------------------------
# Präfix-Tabelle: Alle 23 Präfixe mit Kapitelzuordnung und Beispiel
# ---------------------------------------------------------------------------

_PREFIX_TABLE: list[dict[str, str]] = [
    {"prefix": "META_", "chapter_de": "1. Metadaten", "chapter_en": "1. Metadata", "example": "META_ReportName"},
    {"prefix": "KONTAKT_", "chapter_de": "2. Ansprechpartner", "chapter_en": "2. Contacts", "example": "KONTAKT_BusinessOwner"},
    {"prefix": "BIZ_", "chapter_de": "3. Business-Kontext", "chapter_en": "3. Business Context", "example": "BIZ_Zielsetzung"},
    {"prefix": "KPI_", "chapter_de": "4. KPIs", "chapter_en": "4. KPIs", "example": "KPI_Name"},
    {"prefix": "GLOSSAR_", "chapter_de": "4.3 Glossar", "chapter_en": "4.3 Glossary", "example": "GLOSSAR_Begriff"},
    {"prefix": "DS_", "chapter_de": "5. Datenquellen", "chapter_en": "5. Data Sources", "example": "DS_Quelle"},
    {"prefix": "PQ_", "chapter_de": "6.1 Power Query", "chapter_en": "6.1 Power Query", "example": "PQ_Name"},
    {"prefix": "PQT_", "chapter_de": "6.2 Transformationen", "chapter_en": "6.2 Transformations", "example": "PQT_Abfrage"},
    {"prefix": "PARAM_", "chapter_de": "6.3 Parameter", "chapter_en": "6.3 Parameters", "example": "PARAM_Name"},
    {"prefix": "COL_", "chapter_de": "6.4 Spalten", "chapter_en": "6.4 Columns", "example": "COL_Spalte"},
    {"prefix": "MODELL_", "chapter_de": "7. Modell", "chapter_en": "7. Model", "example": "MODELL_DatasetName"},
    {"prefix": "DAX_", "chapter_de": "7.3 Measures", "chapter_en": "7.3 Measures", "example": "DAX_Measure"},
    {"prefix": "TBLTYP_", "chapter_de": "7.2 Tabellentypen", "chapter_en": "7.2 Table Types", "example": "TBLTYP_Tabelle"},
    {"prefix": "REL_", "chapter_de": "7.2 Beziehungen", "chapter_en": "7.2 Relationships", "example": "REL_Von"},
    {"prefix": "RLS_", "chapter_de": "8.1 Rollen", "chapter_en": "8.1 Roles", "example": "RLS_Rolle"},
    {"prefix": "SEC_", "chapter_de": "8. Sicherheit", "chapter_en": "8. Security", "example": "SEC_RLSBeschreibung"},
    {"prefix": "REFRESH_", "chapter_de": "9.1 Refresh", "chapter_en": "9.1 Refresh", "example": "REFRESH_Typ"},
    {"prefix": "OPS_", "chapter_de": "9. Betrieb", "chapter_en": "9. Operations", "example": "OPS_Monitoring"},
    {"prefix": "ENDORSEMENT_", "chapter_de": "10. Endorsement", "chapter_en": "10. Endorsement", "example": "ENDORSEMENT_Analyse"},
    {"prefix": "VER_", "chapter_de": "11. Versionierung", "chapter_en": "11. Versioning", "example": "VER_Repository"},
    {"prefix": "LOG_", "chapter_de": "11.2 Changelog", "chapter_en": "11.2 Change Log", "example": "LOG_Version"},
    {"prefix": "REVIEW_", "chapter_de": "12. Freigabe", "chapter_en": "12. Approval", "example": "REVIEW_FachName"},
    {"prefix": "DEP_", "chapter_de": "7.2 Abhängige Berichte", "chapter_en": "7.2 Dependent Reports", "example": "DEP_BerichtName"},
]


class DocGenerator:
    """Generiert die Vorlagendokumentation aus mapping_config.py."""

    def __init__(self, language: str = "de") -> None:
        """Initialisiert den Generator für die angegebene Sprache.

        Args:
            language: Sprachcode ("de" oder "en"). Standard ist "de".
        """
        self.language = language
        self.mapping_rules = MAPPING_RULES
        self.always_manual = ALWAYS_MANUAL
        self.availability_matrix = AVAILABILITY_MATRIX

        # Sprachdefinition laden mit Fallback: angeforderte Sprache → "en" → "de"
        if language in doc_labels.LABELS:
            self.labels = doc_labels.LABELS[language]
        elif "en" in doc_labels.LABELS:
            warnings.warn(
                f"Sprachdefinition '{language}' nicht gefunden. "
                f"Fallback auf 'en'.",
                stacklevel=2,
            )
            self.labels = doc_labels.LABELS["en"]
        else:
            warnings.warn(
                f"Sprachdefinition '{language}' nicht gefunden. "
                f"Fallback auf 'de'.",
                stacklevel=2,
            )
            self.labels = doc_labels.LABELS["de"]

    def _get_label(self, *key_path: str) -> str:
        """Gibt einen Label-Wert zurück, mit Fallback auf 'en' und dann 'de'.

        Versucht den Wert zuerst in der aktuellen Sprache zu finden,
        dann in Englisch, dann in Deutsch. Gibt eine Warnung aus wenn
        ein Fallback verwendet wird.

        Args:
            *key_path: Verschachtelte Schlüssel (z.B. "fill_modes", "automatic").

        Returns:
            Den gefundenen Label-Wert als String, oder den letzten Schlüssel
            als Fallback wenn nichts gefunden wird.
        """
        # Versuche aktuelle Sprache
        value = self._resolve_label_path(self.labels, key_path)
        if value is not None:
            return value

        # Fallback: Englisch
        en_labels = doc_labels.LABELS.get("en")
        if en_labels:
            value = self._resolve_label_path(en_labels, key_path)
            if value is not None:
                warnings.warn(
                    f"Label '{'.'.join(key_path)}' nicht in Sprache "
                    f"'{self.language}' gefunden. Fallback auf 'en'.",
                    stacklevel=2,
                )
                return value

        # Fallback: Deutsch
        de_labels = doc_labels.LABELS.get("de")
        if de_labels:
            value = self._resolve_label_path(de_labels, key_path)
            if value is not None:
                warnings.warn(
                    f"Label '{'.'.join(key_path)}' nicht in Sprache "
                    f"'{self.language}' gefunden. Fallback auf 'de'.",
                    stacklevel=2,
                )
                return value

        # Nichts gefunden → letzter Schlüssel als Fallback
        return key_path[-1] if key_path else ""

    @staticmethod
    def _resolve_label_path(labels: dict, key_path: tuple[str, ...]) -> str | None:
        """Löst einen verschachtelten Pfad in einem Label-Dict auf.

        Args:
            labels: Das Label-Dictionary.
            key_path: Tuple von Schlüsseln.

        Returns:
            Den Wert als String oder None wenn nicht gefunden.
        """
        current = labels
        for key in key_path:
            if isinstance(current, dict) and key in current:
                current = current[key]
            else:
                return None
        return current if isinstance(current, str) else None

    def get_category_for_prefix(self, prefix: str) -> str | None:
        """Bestimmt die Hauptkategorie für einen gegebenen Präfix.

        Delegiert an doc_categories.get_category_for_prefix.

        Args:
            prefix: Der Platzhalter-Präfix (z.B. "META_", "DS_").

        Returns:
            Den Kategorie-Schlüssel oder None.
        """
        return get_category_for_prefix(prefix)

    def get_subcategory_for_prefix(self, prefix: str) -> str | None:
        """Bestimmt die Unterkategorie für einen gegebenen Präfix.

        Delegiert an doc_categories.get_subcategory_for_prefix.

        Args:
            prefix: Der Platzhalter-Präfix (z.B. "META_", "PQ_").

        Returns:
            Den Unterkategorie-Schlüssel oder None.
        """
        return get_subcategory_for_prefix(prefix)

    def get_fill_mode(self, placeholder_name: str) -> str:
        """Bestimmt ob ein Platzhalter automatisch oder manuell befüllt wird.

        Logik:
        - Platzhalter in ALWAYS_MANUAL → "manuell"
        - Platzhalter in MAPPING_RULES UND Root in mindestens einem
          AVAILABILITY_MATRIX-Eintrag → "automatisch"
        - Sonst → "manuell"

        Args:
            placeholder_name: Der Platzhalter-Name (z.B. "META_ReportName").

        Returns:
            Den lokalisierten Befüllungsmodus ("automatisch"/"manuell" bzw.
            "automatic"/"manual").
        """
        fill_modes = self.labels["fill_modes"]

        # ALWAYS_MANUAL hat Vorrang
        if placeholder_name in self.always_manual:
            return fill_modes["manual"]

        # Prüfe ob in MAPPING_RULES und Root verfügbar
        if placeholder_name in self.mapping_rules:
            metadata_path = self.mapping_rules[placeholder_name]
            root = self._extract_root(metadata_path)

            # Prüfe ob Root in mindestens einem Verbindungstyp verfügbar ist
            for available_roots in self.availability_matrix.values():
                if root in available_roots:
                    return fill_modes["automatic"]

        return fill_modes["manual"]

    def get_availability(self, placeholder_name: str) -> dict[str, bool]:
        """Ermittelt die Verfügbarkeit eines Platzhalters pro Verbindungstyp.

        Prüft für jeden Verbindungstyp (fabric, local, pbip), ob der
        Root-Pfad des Platzhalters in der AVAILABILITY_MATRIX enthalten ist.

        Args:
            placeholder_name: Der Platzhalter-Name (z.B. "DS_Quelle").

        Returns:
            Dict mit Verfügbarkeit pro Verbindungstyp,
            z.B. {"fabric": True, "local": True, "pbip": True}.
            Alle False, wenn Platzhalter nicht in MAPPING_RULES oder
            wenn der Metadaten-Pfad leer/ungültig ist.
        """
        # Wenn nicht in MAPPING_RULES → alle False
        if placeholder_name not in self.mapping_rules:
            return {conn_type: False for conn_type in self.availability_matrix}

        metadata_path = self.mapping_rules[placeholder_name]
        root = self._extract_root(metadata_path)

        # Ungültiger/leerer Metadaten-Pfad → alle False
        if not root:
            return {conn_type: False for conn_type in self.availability_matrix}

        return {
            conn_type: root in available_roots
            for conn_type, available_roots in self.availability_matrix.items()
        }

    # ------------------------------------------------------------------
    # Hauptmethode: generate()
    # ------------------------------------------------------------------

    def generate(self) -> str:
        """Generiert die vollständige Dokumentation als Markdown-String.

        Baut den Markdown-Text aus einzelnen Abschnitten zusammen:
        1. Titel (H1)
        2. Übersicht
        3. Vorlage ohne Rebuild ändern
        4. Neue manuelle Platzhalter hinzufügen
        5. Platzhalter-Namenskonvention
        6. Beschreibende Metadaten
        7. Backenddaten (Stub für Task 4.3)
        8. Frontenddaten (Stub für Task 4.4)
        9. Sonstige (nur bei unbekannten Präfixen)
        10. Abschließende Abschnitte (Stub für Task 4.5)

        Returns:
            Vollständigen Markdown-String der Dokumentation.
        """
        sections = [
            self._generate_title(),
            self._generate_overview(),
            self._generate_vorlage_ohne_rebuild(),
            self._generate_manuelle_platzhalter(),
            self._generate_namenskonvention(),
            self._generate_beschreibende_metadaten(),
            self._generate_backenddaten(),
            self._generate_frontenddaten(),
            self._generate_sonstige(),
            self._generate_closing_sections(),
        ]

        # Leere Abschnitte herausfiltern, zusammenfügen
        result = "\n\n".join(section for section in sections if section)

        # Validierung als Self-Check
        validation_warnings = self.validate(generated_output=result)
        if validation_warnings:
            for msg in validation_warnings:
                warnings.warn(msg, stacklevel=2)

        return result

    # ------------------------------------------------------------------
    # Abschnitts-Generatoren
    # ------------------------------------------------------------------

    def _generate_title(self) -> str:
        """Generiert die H1-Überschrift aus den Labels."""
        return f"# {self.labels['title']}"

    def _generate_overview(self) -> str:
        """Generiert den Übersichts-Abschnitt.

        Enthält eine Einleitung, die das Platzhalter-Format und den
        Zweck der Dokumentationsvorlage beschreibt.
        """
        title = self.labels["section_titles"]["uebersicht"]

        if self.language == "de":
            intro = (
                "Die Datei `PowerBI_Dokumentationsvorlage.md` definiert die Struktur "
                "der generierten Dokumentation. Sie enthält Platzhalter im Format "
                "`{{PRÄFIX_Name}}`, die beim Generieren durch echte Werte ersetzt werden."
            )
        else:
            intro = (
                "The file `PowerBI_Dokumentationsvorlage.md` defines the structure "
                "of the generated documentation. It contains placeholders in the format "
                "`{{PREFIX_Name}}` that are replaced with actual values during generation."
            )

        return f"## {title}\n\n{intro}"

    def _generate_vorlage_ohne_rebuild(self) -> str:
        """Generiert den Abschnitt 'Vorlage ohne Rebuild ändern'.

        Beschreibt die Schritte: Vorlage kopieren, bearbeiten, EXE starten.
        """
        title = self.labels["section_titles"]["vorlage_ohne_rebuild"]

        if self.language == "de":
            steps = (
                "1. Kopiere `PowerBI_Dokumentationsvorlage.md` neben die `PowerBI-AutoDoku.exe`\n"
                "2. Bearbeite die Kopie mit einem Texteditor\n"
                "3. Starte die EXE — sie nutzt automatisch die Datei neben der EXE statt der eingebetteten"
            )
        else:
            steps = (
                "1. Copy `PowerBI_Dokumentationsvorlage.md` next to the `.exe` file\n"
                "2. Edit the copy with a text editor\n"
                "3. Start the EXE — it automatically uses the file next to the EXE instead of the embedded one"
            )

        return f"## {title}\n\n{steps}"

    def _generate_manuelle_platzhalter(self) -> str:
        """Generiert den Abschnitt 'Neue manuelle Platzhalter hinzufügen'.

        Beschreibt wie man benutzerdefinierte Platzhalter mit CUSTOM_-Präfix
        hinzufügt, inklusive eines Codebeispiels.
        """
        title = self.labels["section_titles"]["neue_manuelle_platzhalter"]

        if self.language == "de":
            intro = (
                "Manuelle Platzhalter erscheinen im Formular als Eingabefelder. "
                "Du kannst sie ohne Code-Änderung hinzufügen:"
            )
            steps = (
                "1. Öffne `PowerBI_Dokumentationsvorlage.md`\n"
                "2. Füge einen neuen Platzhalter ein, z.B. `{{CUSTOM_MeinFeld}}`\n"
                "3. Verwende ein eindeutiges Präfix (z.B. `CUSTOM_`) um Konflikte zu vermeiden\n"
                "4. Speichere die Datei neben der EXE"
            )
            example_label = "**Beispiel:**"
            example_code = (
                "```markdown\n"
                "### 12. Anhänge & Referenzen\n"
                "- Projektleiter: {{CUSTOM_Projektleiter}}\n"
                "- Freigabedatum: {{CUSTOM_Freigabedatum}}\n"
                "```"
            )
            outro = "Diese Felder erscheinen automatisch im Formular unter dem jeweiligen Kapitel."
        else:
            intro = (
                "Manual placeholders appear in the form as input fields. "
                "You can add them without code changes:"
            )
            steps = (
                "1. Open `PowerBI_Dokumentationsvorlage.md`\n"
                "2. Add a new placeholder, e.g. `{{CUSTOM_MyField}}`\n"
                "3. Use a unique prefix (e.g. `CUSTOM_`) to avoid conflicts\n"
                "4. Save the file next to the EXE"
            )
            example_label = "**Example:**"
            example_code = (
                "```markdown\n"
                "### 12. Attachments & References\n"
                "- Project Lead: {{CUSTOM_ProjectLead}}\n"
                "- Approval Date: {{CUSTOM_ApprovalDate}}\n"
                "```"
            )
            outro = "These fields automatically appear in the form under the respective chapter."

        return (
            f"## {title}\n\n"
            f"{intro}\n\n"
            f"{steps}\n\n"
            f"{example_label}\n\n"
            f"{example_code}\n\n"
            f"{outro}"
        )

    def _generate_namenskonvention(self) -> str:
        """Generiert den Abschnitt 'Platzhalter-Namenskonvention'.

        Enthält eine Erklärung des Präfix-Schemas und eine Tabelle mit
        allen 23 Präfixen, deren Kapitelzuordnung und einem Beispiel.
        """
        title = self.labels["section_titles"]["namenskonvention"]

        if self.language == "de":
            intro = (
                "Alle Platzhalter folgen dem Schema `{{PRÄFIX_Name}}`. "
                "Das Präfix wird im Formular automatisch entfernt."
            )
            header = "| Präfix | Kapitel | Beispiel |"
        else:
            intro = (
                "All placeholders follow the pattern `{{PREFIX_Name}}`. "
                "The prefix is automatically removed in the form display."
            )
            header = "| Prefix | Chapter | Example |"

        separator = "|--------|---------|---------|"

        # Tabellenzeilen aus _PREFIX_TABLE generieren
        rows: list[str] = []
        chapter_key = "chapter_de" if self.language == "de" else "chapter_en"
        for entry in _PREFIX_TABLE:
            rows.append(
                f"| {entry['prefix']} | {entry[chapter_key]} | {entry['example']} |"
            )

        table = "\n".join([header, separator] + rows)

        return f"## {title}\n\n{intro}\n\n{table}"

    # ------------------------------------------------------------------
    # Kategorie-Generatoren
    # ------------------------------------------------------------------

    def _generate_beschreibende_metadaten(self) -> str:
        """Generiert den Abschnitt 'Beschreibende Metadaten'.

        Enthält eine H2-Überschrift, eine Kategorie-Beschreibung und
        pro Unterkategorie eine H3-Überschrift mit Tabelle
        (Platzhalter, Beschreibung, Befüllung).
        """
        category_key = "beschreibende_metadaten"
        category_def = CATEGORIES[category_key]
        subcategories = category_def["subcategories"]

        # H2-Überschrift
        subcategory_names = self.labels["subcategory_names"]
        if self.language == "de":
            h2_title = "Beschreibende Metadaten"
        else:
            h2_title = "Descriptive Metadata"

        lines: list[str] = [f"## {h2_title}"]

        # Kategorie-Beschreibung (1-3 Sätze)
        description = self.labels["category_descriptions"][category_key]
        lines.append("")
        lines.append(description)

        # Feste Reihenfolge der Unterkategorien
        subcategory_order = [
            "bericht_metadaten",
            "ansprechpartner",
            "business_kontext",
            "kpis_glossar",
            "versionierung",
            "freigabe",
        ]

        # Tabellen-Header aus Labels
        table_headers = self.labels["table_headers"][category_key]

        for subcat_key in subcategory_order:
            prefixes = subcategories[subcat_key]
            display_name = subcategory_names[subcat_key]

            # Präfixe als komma-getrennte Anzeige
            prefix_display = ", ".join(prefixes)

            # H3-Überschrift
            lines.append("")
            lines.append(f"### {display_name} ({prefix_display})")
            lines.append("")

            # Platzhalter für diese Unterkategorie sammeln
            placeholders = self._collect_placeholders_for_subcategory(prefixes)

            # Tabelle generieren
            header_line = "| " + " | ".join(table_headers) + " |"
            separator_line = "|" + "|".join("---" for _ in table_headers) + "|"
            lines.append(header_line)
            lines.append(separator_line)

            for ph_name in placeholders:
                desc = self._get_placeholder_description(ph_name)
                fill_mode = self.get_fill_mode(ph_name)
                lines.append(f"| {ph_name} | {desc} | {fill_mode} |")

        return "\n".join(lines)

    def _collect_placeholders_for_subcategory(
        self, prefixes: list[str]
    ) -> list[str]:
        """Sammelt alle Platzhalter, die zu den gegebenen Präfixen gehören.

        Iteriert über MAPPING_RULES-Keys und ALWAYS_MANUAL-Einträge,
        extrahiert den Präfix und prüft Zugehörigkeit. Ergebnis ist
        alphabetisch sortiert und dedupliziert.

        Args:
            prefixes: Liste von Präfixen (z.B. ["META_"]).

        Returns:
            Alphabetisch sortierte Liste der Platzhalter-Namen.
        """
        all_placeholders: set[str] = set()

        # Alle Platzhalter-Quellen durchlaufen
        all_names = list(self.mapping_rules.keys()) + self.always_manual

        for name in all_names:
            prefix = self._extract_prefix(name)
            if prefix in prefixes:
                all_placeholders.add(name)

        return sorted(all_placeholders)

    def _get_placeholder_description(self, placeholder_name: str) -> str:
        """Gibt die Beschreibung für einen Platzhalter zurück (max 120 Zeichen).

        Args:
            placeholder_name: Der Platzhalter-Name.

        Returns:
            Die lokalisierte Beschreibung oder einen Fallback-Text.
        """
        if placeholder_name in _PLACEHOLDER_DESCRIPTIONS:
            desc_entry = _PLACEHOLDER_DESCRIPTIONS[placeholder_name]
            desc = desc_entry.get(self.language, desc_entry.get("de", ""))
        else:
            # Fallback: Platzhalter-Name ohne Präfix als Beschreibung
            prefix = self._extract_prefix(placeholder_name)
            desc = placeholder_name.replace(prefix, "").replace("_", " ") if prefix else placeholder_name

        # Auf 120 Zeichen begrenzen
        if len(desc) > 120:
            desc = desc[:117] + "..."

        return desc

    @staticmethod
    def _extract_prefix(placeholder_name: str) -> str:
        """Extrahiert den Präfix aus einem Platzhalter-Namen.

        Der Präfix ist alles bis einschließlich dem ersten Unterstrich.
        Bei speziellen Fällen wie GLOSSAR_ wird das korrekte Präfix erkannt.

        Args:
            placeholder_name: Der Platzhalter-Name (z.B. "META_ReportName").

        Returns:
            Den Präfix (z.B. "META_") oder leeren String.
        """
        # Bekannte Präfixe prüfen (längste zuerst für korrekte Zuordnung)
        known_prefixes = sorted(
            CATEGORIES["beschreibende_metadaten"]["prefixes"]
            + CATEGORIES["backenddaten"]["prefixes"],
            key=len,
            reverse=True,
        )
        for prefix in known_prefixes:
            if isinstance(prefix, str) and placeholder_name.startswith(prefix):
                return prefix

        # Fallback: Split am ersten Unterstrich
        if "_" in placeholder_name:
            return placeholder_name.split("_")[0] + "_"
        return ""

    def _generate_backenddaten(self) -> str:
        """Generiert den Abschnitt 'Backenddaten'.

        Enthält eine H2-Überschrift, eine Kategorie-Beschreibung und
        pro Unterkategorie (7 Stück) eine H3-Überschrift mit Tabelle.
        Tabellen-Spalten: Platzhalter, Metadaten-Pfad, Beschreibung,
        Fabric (✓/✗), Lokal (✓/✗), PBIP (✓/✗).
        """
        category_key = "backenddaten"
        category_def = CATEGORIES[category_key]
        subcategories = category_def["subcategories"]

        # H2-Überschrift
        if self.language == "de":
            h2_title = "Backenddaten"
        else:
            h2_title = "Backend Data"

        lines: list[str] = [f"## {h2_title}"]

        # Kategorie-Beschreibung (1-3 Sätze)
        description = self.labels["category_descriptions"][category_key]
        lines.append("")
        lines.append(description)

        # Feste Reihenfolge der Unterkategorien
        subcategory_order = [
            "datenquellen",
            "power_query",
            "datenmodell",
            "measures",
            "sicherheit_rls",
            "betrieb_refresh",
            "endorsement",
        ]

        # Tabellen-Header aus Labels
        table_headers = self.labels["table_headers"][category_key]

        # Verfügbarkeitsindikatoren
        available = self.labels["availability_indicators"]["available"]
        not_available = self.labels["availability_indicators"]["not_available"]

        subcategory_names = self.labels["subcategory_names"]

        for subcat_key in subcategory_order:
            prefixes = subcategories[subcat_key]
            display_name = subcategory_names[subcat_key]

            # Präfixe als komma-getrennte Anzeige
            prefix_display = ", ".join(prefixes)

            # H3-Überschrift
            lines.append("")
            lines.append(f"### {display_name} ({prefix_display})")
            lines.append("")

            # Platzhalter für diese Unterkategorie sammeln
            placeholders = self._collect_placeholders_for_subcategory(prefixes)

            # Tabelle generieren
            header_line = "| " + " | ".join(table_headers) + " |"
            separator_line = "|" + "|".join("---" for _ in table_headers) + "|"
            lines.append(header_line)
            lines.append(separator_line)

            for ph_name in placeholders:
                desc = self._get_placeholder_description(ph_name)

                # Metadaten-Pfad bestimmen
                if ph_name in self.mapping_rules:
                    metadata_path = self.mapping_rules[ph_name]
                else:
                    # ALWAYS_MANUAL-Einträge ohne Mapping
                    metadata_path = "\u2014"

                # Verfügbarkeit bestimmen
                availability = self.get_availability(ph_name)
                fabric_indicator = available if availability.get("fabric", False) else not_available
                local_indicator = available if availability.get("local", False) else not_available
                pbip_indicator = available if availability.get("pbip", False) else not_available

                lines.append(
                    f"| {ph_name} | {metadata_path} | {desc} "
                    f"| {fabric_indicator} | {local_indicator} | {pbip_indicator} |"
                )

        return "\n".join(lines)

    def _generate_frontenddaten(self) -> str:
        """Generiert den Abschnitt 'Frontenddaten'.

        Enthält drei Unterkategorien:
        - Visuals und Seiten (mit MODELL_Visuals-Tabelle)
        - Datenschnitte und Filter (CUSTOM_-Hinweis)
        - Interaktionen und Drilldowns (CUSTOM_-Hinweis)
        """
        parts: list[str] = []

        # H2 Überschrift – Kategorie-Name aus subcategory_names wäre
        # zu granular, wir verwenden den Kategorienamen direkt
        if self.language == "de":
            heading = "Frontenddaten"
        else:
            heading = "Frontend Data"
        parts.append(f"## {heading}")

        # Kategorie-Beschreibung
        description = self.labels["category_descriptions"]["frontenddaten"]
        parts.append(description)

        # --- Unterkategorie 1: Visuals und Seiten ---
        subcat_name = self.labels["subcategory_names"]["visuals_seiten"]
        parts.append(f"### {subcat_name}")

        # Tabelle mit MODELL_Visuals
        headers = self.labels["table_headers"]["frontenddaten"]
        header_row = "| " + " | ".join(headers) + " |"
        separator = "| " + " | ".join("---" for _ in headers) + " |"

        fill_auto = self.labels["fill_modes"]["automatic"]

        if self.language == "de":
            generated_section_desc = "Berichtsseiten-Übersicht, Visualtypen, Detail-Tabelle"
        else:
            generated_section_desc = "Report pages overview, visual types, detail table"

        data_row = f"| MODELL_Visuals | {fill_auto} | {generated_section_desc} |"

        table = "\n".join([header_row, separator, data_row])
        parts.append(table)

        # --- Unterkategorie 2: Datenschnitte und Filter ---
        subcat_name = self.labels["subcategory_names"]["datenschnitte_filter"]
        parts.append(f"### {subcat_name}")

        if self.language == "de":
            custom_hint = (
                "Für diese Unterkategorie existieren keine eigenen automatischen Platzhalter. "
                "Sie kann über benutzerdefinierte `CUSTOM_`-Platzhalter erweitert werden."
            )
            example_label = "**Beispiel:**"
        else:
            custom_hint = (
                "No dedicated automatic placeholders exist for this subcategory. "
                "It can be extended using custom `CUSTOM_` placeholders."
            )
            example_label = "**Example:**"

        parts.append(custom_hint)
        parts.append(f"{example_label} `CUSTOM_FilterBeschreibung`")

        # --- Unterkategorie 3: Interaktionen und Drilldowns ---
        subcat_name = self.labels["subcategory_names"]["interaktionen_drilldowns"]
        parts.append(f"### {subcat_name}")

        if self.language == "de":
            custom_hint = (
                "Für diese Unterkategorie existieren keine eigenen automatischen Platzhalter. "
                "Sie kann über benutzerdefinierte `CUSTOM_`-Platzhalter erweitert werden."
            )
            example_label = "**Beispiel:**"
        else:
            custom_hint = (
                "No dedicated automatic placeholders exist for this subcategory. "
                "It can be extended using custom `CUSTOM_` placeholders."
            )
            example_label = "**Example:**"

        parts.append(custom_hint)
        parts.append(f"{example_label} `CUSTOM_DrilldownPfad`")

        return "\n\n".join(parts)

    def _generate_sonstige(self) -> str:
        """Generiert den Abschnitt 'Sonstige' für unkategorisierte Platzhalter.

        Sammelt alle Platzhalter aus MAPPING_RULES und ALWAYS_MANUAL, deren
        Präfix keiner bekannten Kategorie zugeordnet ist. Gibt eine Warnung
        aus und listet sie in einem separaten Abschnitt auf.

        Returns:
            Markdown-String mit dem Sonstige-Abschnitt oder leeren String,
            wenn keine unkategorisierten Platzhalter existieren.
        """
        # Alle bekannten Präfixe sammeln
        known_prefixes: set[str] = set()
        for category_def in CATEGORIES.values():
            for prefix in category_def["prefixes"]:
                known_prefixes.add(prefix)

        # Unkategorisierte Platzhalter finden
        uncategorized: list[str] = []
        all_names = list(self.mapping_rules.keys()) + self.always_manual

        for name in all_names:
            prefix = self._extract_prefix(name)
            if prefix and prefix not in known_prefixes:
                uncategorized.append(name)
            elif not prefix and name:
                # Kein Präfix extrahierbar
                uncategorized.append(name)

        # Deduplizieren und sortieren
        uncategorized = sorted(set(uncategorized))

        if not uncategorized:
            return ""

        # Warnung ausgeben für jeden unbekannten Präfix
        unknown_prefixes = set()
        for name in uncategorized:
            prefix = self._extract_prefix(name)
            if prefix:
                unknown_prefixes.add(prefix)

        for prefix in sorted(unknown_prefixes):
            warnings.warn(
                f"Unbekannter Präfix '{prefix}': Platzhalter werden im "
                f"'Sonstige'-Abschnitt aufgeführt.",
                stacklevel=2,
            )

        # Abschnitt generieren
        if self.language == "de":
            heading = "Sonstige"
            intro = (
                "Die folgenden Platzhalter konnten keiner der drei "
                "Hauptkategorien zugeordnet werden."
            )
            table_headers = ["Platzhalter", "Beschreibung", "Befüllung"]
        else:
            heading = "Other"
            intro = (
                "The following placeholders could not be assigned to any "
                "of the three main categories."
            )
            table_headers = ["Placeholder", "Description", "Fill Mode"]

        lines: list[str] = [f"## {heading}"]
        lines.append("")
        lines.append(intro)
        lines.append("")

        # Tabelle
        header_line = "| " + " | ".join(table_headers) + " |"
        separator_line = "|" + "|".join("---" for _ in table_headers) + "|"
        lines.append(header_line)
        lines.append(separator_line)

        for ph_name in uncategorized:
            desc = self._get_placeholder_description(ph_name)
            fill_mode = self.get_fill_mode(ph_name)
            lines.append(f"| {ph_name} | {desc} | {fill_mode} |")

        return "\n".join(lines)

    def _generate_closing_sections(self) -> str:
        """Generiert die abschließenden Abschnitte.

        Enthält zwei Abschnitte:
        1. Automatisch generierte Abschnitte – Tabelle mit DS_Architektur,
           DS_Lineage, MODELL_Visuals
        2. Tabellen-Platzhalter – Erklärung der Tabellen-Expansion
        """
        section_auto = self._generate_auto_generated_sections()
        section_table = self._generate_table_placeholders()
        return f"{section_auto}\n\n{section_table}"

    def _generate_auto_generated_sections(self) -> str:
        """Generiert den Abschnitt 'Automatisch generierte Abschnitte'.

        Enthält eine Markdown-Tabelle mit den drei generierten Platzhaltern
        DS_Architektur, DS_Lineage und MODELL_Visuals samt Beschreibung.
        """
        title = self.labels["section_titles"]["automatisch_generierte_abschnitte"]

        if self.language == "de":
            intro = "Diese Platzhalter werden durch generierte Markdown-Inhalte ersetzt:"
            header = "| Platzhalter | Inhalt |"
            rows = [
                "| DS_Architektur | Datenarchitektur (Quellsysteme, Schichten, Speichermodi, Diagramm) |",
                "| DS_Lineage | Mermaid-Datenlineage-Diagramm |",
                "| MODELL_Visuals | Visuals-Übersicht (Seiten, Typen, Datenschnitte, Detail-Tabelle) |",
            ]
        else:
            intro = "These placeholders are replaced with generated Markdown content:"
            header = "| Placeholder | Content |"
            rows = [
                "| DS_Architektur | Data Architecture (source systems, layers, storage modes, diagram) |",
                "| DS_Lineage | Mermaid data lineage diagram |",
                "| MODELL_Visuals | Visuals overview (pages, types, slicers, detail table) |",
            ]

        separator = "|-------------|---------|"
        table = "\n".join([header, separator] + rows)

        return f"## {title}\n\n{intro}\n\n{table}"

    def _generate_table_placeholders(self) -> str:
        """Generiert den Abschnitt 'Tabellen-Platzhalter'.

        Erklärt die automatische Expansion von Platzhaltern in
        Markdown-Tabellenzeilen.
        """
        title = self.labels["section_titles"]["tabellen_platzhalter"]

        if self.language == "de":
            content = (
                "Platzhalter in Markdown-Tabellenzeilen werden automatisch expandiert. "
                "Wenn die Datenquelle mehrere Einträge hat (z.B. 5 Tabellen), "
                "wird die Tabellenzeile 5-mal dupliziert.\n\n"
                "**Wichtig:** Platzhalter in der gleichen Tabellenzeile müssen zum gleichen "
                "Datenobjekt gehören (z.B. alle DS_-Platzhalter in einer Zeile beziehen sich "
                "auf Datenquellen)."
            )
        else:
            content = (
                "Placeholders in Markdown table rows are automatically expanded. "
                "When the data source has multiple entries (e.g. 5 tables), "
                "the table row is duplicated 5 times.\n\n"
                "**Important:** Placeholders in the same table row must belong to the same "
                "data object (e.g. all DS_ placeholders in a row refer to data sources)."
            )

        return f"## {title}\n\n{content}"

    # ------------------------------------------------------------------
    # Hilfsmethoden
    # ------------------------------------------------------------------

    @staticmethod
    def _extract_root(metadata_path: str) -> str:
        """Extrahiert den Root aus einem Metadaten-Pfad.

        Regel: Split auf '[', dann Split auf '.', erstes Segment nehmen.
        Bei leerem oder ungültigem Pfad wird ein leerer String zurückgegeben.

        Beispiele:
            "data_sources[*].name" → "data_sources"
            "refresh_schedule.frequency" → "refresh_schedule"
            "report_name" → "report_name"
            "" → ""
            "   " → ""

        Args:
            metadata_path: Der Metadaten-Pfad aus MAPPING_RULES.

        Returns:
            Den Root-Bezeichner oder leeren String bei ungültigem Pfad.
        """
        if not metadata_path or not metadata_path.strip():
            return ""

        # Split auf '[' → nimm den Teil vor der Klammer
        before_bracket = metadata_path.split("[")[0]
        # Split auf '.' → nimm das erste Segment
        root = before_bracket.split(".")[0]

        # Prüfe ob ein gültiges Segment extrahiert wurde
        if not root or not root.strip():
            return ""

        return root

    # ------------------------------------------------------------------
    # Validierung
    # ------------------------------------------------------------------

    def validate(self, generated_output: str | None = None) -> list[str]:
        """Validates the generator output for correctness.

        Performs the following checks:
        1. Completeness: all placeholders from MAPPING_RULES + ALWAYS_MANUAL
           appear in the generated output.
        2. Uniqueness: no placeholder appears in more than one subcategory.
        3. Structure: both DE and EN versions have identical table row counts.
        4. Prefix: all known prefixes appear in the Namenskonvention table.

        Args:
            generated_output: Optional pre-generated output to validate.
                If None, calls self.generate() internally (without re-triggering
                validation to avoid recursion).

        Returns:
            List of warning/error messages. Empty list means all validations passed.
        """
        issues: list[str] = []

        # --- 1. Vollständigkeitsprüfung ---
        if generated_output is None:
            # Generate without triggering validate again
            output = self._generate_raw()
        else:
            output = generated_output

        all_expected_placeholders = set(self.mapping_rules.keys()) | set(self.always_manual)
        for placeholder in sorted(all_expected_placeholders):
            if placeholder not in output:
                issues.append(
                    f"Vollständigkeit: Platzhalter '{placeholder}' fehlt in der generierten Ausgabe."
                )

        # --- 2. Eindeutigkeitsprüfung ---
        placeholder_to_subcategories: dict[str, list[str]] = {}
        for category_key, category_def in CATEGORIES.items():
            for subcat_key, prefixes in category_def["subcategories"].items():
                if not prefixes:
                    continue
                placeholders_in_subcat = self._collect_placeholders_for_subcategory(prefixes)
                for ph in placeholders_in_subcat:
                    if ph not in placeholder_to_subcategories:
                        placeholder_to_subcategories[ph] = []
                    placeholder_to_subcategories[ph].append(f"{category_key}/{subcat_key}")

        for ph, subcats in sorted(placeholder_to_subcategories.items()):
            if len(subcats) > 1:
                subcats_str = ", ".join(subcats)
                issues.append(
                    f"Eindeutigkeit: Platzhalter '{ph}' erscheint in mehreren "
                    f"Unterkategorien: {subcats_str}."
                )

        # --- 3. Strukturprüfung ---
        de_gen = DocGenerator(language="de")
        en_gen = DocGenerator(language="en")
        de_output = de_gen._generate_raw()
        en_output = en_gen._generate_raw()

        de_table_rows = self._count_table_rows_per_category(de_output)
        en_table_rows = self._count_table_rows_per_category(en_output)

        # Compare total table row counts
        de_total = sum(de_table_rows.values())
        en_total = sum(en_table_rows.values())
        if de_total != en_total:
            issues.append(
                f"Struktur: DE-Version hat {de_total} Tabellenzeilen, "
                f"EN-Version hat {en_total} Tabellenzeilen."
            )

        # --- 4. Präfix-Prüfung ---
        # Collect all known prefixes from CATEGORIES
        all_known_prefixes: set[str] = set()
        for category_def in CATEGORIES.values():
            for prefix in category_def["prefixes"]:
                all_known_prefixes.add(prefix)

        # Find the Namenskonvention section in the output
        namenskonvention_section = self._extract_namenskonvention_section(output)
        for prefix in sorted(all_known_prefixes):
            if prefix not in namenskonvention_section:
                issues.append(
                    f"Präfix: Präfix '{prefix}' fehlt in der Namenskonvention-Tabelle."
                )

        return issues

    def _generate_raw(self) -> str:
        """Generates output without triggering validation (avoids recursion).

        Returns:
            Vollständigen Markdown-String der Dokumentation.
        """
        sections = [
            self._generate_title(),
            self._generate_overview(),
            self._generate_vorlage_ohne_rebuild(),
            self._generate_manuelle_platzhalter(),
            self._generate_namenskonvention(),
            self._generate_beschreibende_metadaten(),
            self._generate_backenddaten(),
            self._generate_frontenddaten(),
            self._generate_sonstige(),
            self._generate_closing_sections(),
        ]
        return "\n\n".join(section for section in sections if section)

    @staticmethod
    def _count_table_rows_per_category(output: str) -> dict[str, int]:
        """Counts table rows (lines containing '|') in category sections.

        Only counts rows in the main category sections (Beschreibende Metadaten,
        Backenddaten, Frontenddaten).

        Args:
            output: The generated markdown output.

        Returns:
            Dict mapping section name to table row count.
        """
        result: dict[str, int] = {}
        lines = output.split("\n")
        current_section: str | None = None

        for line in lines:
            stripped = line.strip()
            # Detect H2 sections
            if stripped.startswith("## "):
                section_name = stripped[3:].strip()
                # Check if it's one of the category sections
                if any(
                    keyword in section_name.lower()
                    for keyword in [
                        "beschreibende", "descriptive",
                        "backenddaten", "backend",
                        "frontenddaten", "frontend",
                    ]
                ):
                    current_section = section_name
                    result[current_section] = 0
                else:
                    current_section = None
            elif current_section and "|" in stripped:
                # Skip header separator lines (|---|---|...)
                if not all(c in "|-: " for c in stripped):
                    result[current_section] += 1

        return result

    @staticmethod
    def _extract_namenskonvention_section(output: str) -> str:
        """Extracts the Namenskonvention section from the generated output.

        Args:
            output: The generated markdown output.

        Returns:
            The text content of the Namenskonvention section.
        """
        lines = output.split("\n")
        in_section = False
        section_lines: list[str] = []

        for line in lines:
            stripped = line.strip()
            if stripped.startswith("## "):
                if in_section:
                    break  # End of the section
                # Check if this is the Namenskonvention section
                if any(
                    keyword in stripped.lower()
                    for keyword in ["namenskonvention", "naming convention"]
                ):
                    in_section = True
            if in_section:
                section_lines.append(line)

        return "\n".join(section_lines)
