"""Metadaten-Mapper für die Zuordnung von Platzhaltern zu Metadaten.

Ordnet extrahierte Power BI Metadaten den Platzhaltern der
Dokumentationsvorlage zu und erstellt eine Kategorisierungsübersicht.
"""
from __future__ import annotations

from core.models import (
    CategorySummary,
    Placeholder,
    PlaceholderCategory,
    PlaceholderMapping,
    ReportMetadata,
)
from core.mapping_config import ALWAYS_MANUAL, MAPPING_RULES
from core.table_expander import get_table_placeholder_names

# Platzhalter, die durch Tabellen-Expansion behandelt werden
_TABLE_PLACEHOLDERS = get_table_placeholder_names()


class MetadataMapper:
    """Ordnet extrahierte Metadaten den Platzhaltern der Vorlage zu."""

    def __init__(
        self,
        mapping_rules: dict[str, str] | None = None,
        always_manual: list[str] | None = None,
    ) -> None:
        self._mapping_rules = mapping_rules if mapping_rules is not None else MAPPING_RULES
        self._always_manual = always_manual if always_manual is not None else ALWAYS_MANUAL

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def map(
        self,
        placeholders: list[Placeholder],
        metadata: ReportMetadata,
    ) -> list[PlaceholderMapping]:
        """Ordnet Metadaten den Platzhaltern zu.

        Logik pro Platzhalter:
        - In ALWAYS_MANUAL → source="manual", value=None
        - In MAPPING_RULES und Metadatum vorhanden → source="auto", value=…
        - In MAPPING_RULES aber Metadatum fehlt → source="manual", value=None
        - Sonst → source="manual", value=None
        """
        mappings: list[PlaceholderMapping] = []

        for ph in placeholders:
            name = ph.name

            # 1) Immer manuell?
            if name in self._always_manual:
                mappings.append(
                    PlaceholderMapping(placeholder=ph, value=None, source="manual")
                )
                continue

            # 2) Zuordnungsregel vorhanden?
            if name in self._mapping_rules:
                path = self._mapping_rules[name]

                # Tabellen-Platzhalter: werden durch Table Expander behandelt
                if name in _TABLE_PLACEHOLDERS and "[*]" in path:
                    # Prüfen ob Daten vorhanden sind
                    resolved = self._resolve_path(metadata, path)
                    if resolved is not None:
                        mappings.append(
                            PlaceholderMapping(
                                placeholder=ph,
                                value=f"[Tabelle: {resolved[:60]}{'…' if len(resolved) > 60 else ''}]",
                                source="auto",
                            )
                        )
                    else:
                        mappings.append(
                            PlaceholderMapping(placeholder=ph, value=None, source="manual")
                        )
                    continue

                resolved = self._resolve_path(metadata, path)
                if resolved is not None:
                    mappings.append(
                        PlaceholderMapping(placeholder=ph, value=resolved, source="auto")
                    )
                else:
                    mappings.append(
                        PlaceholderMapping(placeholder=ph, value=None, source="manual")
                    )
                continue

            # 3) Kein Mapping bekannt → manuell
            mappings.append(
                PlaceholderMapping(placeholder=ph, value=None, source="manual")
            )

        return mappings

    def get_category_summary(
        self, mappings: list[PlaceholderMapping]
    ) -> CategorySummary:
        """Erstellt Übersicht: auto / manual / partial."""
        auto_count = 0
        manual_count = 0
        partial_count = 0
        details: list[PlaceholderCategory] = []

        for m in mappings:
            if m.source == "auto":
                auto_count += 1
                cat = "automatisch"
            elif m.source == "partial":
                partial_count += 1
                cat = "teilweise"
            else:
                manual_count += 1
                cat = "manuell"

            details.append(
                PlaceholderCategory(
                    placeholder_name=m.placeholder.name,
                    category=cat,
                )
            )

        return CategorySummary(
            auto_count=auto_count,
            manual_count=manual_count,
            partial_count=partial_count,
            details=details,
        )

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _resolve_path(
        self, metadata: ReportMetadata, path: str
    ) -> str | None:
        """Löst einen Metadaten-Pfad zu einem konkreten Wert auf.

        Pfad-Konventionen (aus mapping_config):
          - ``"field"``            → ``getattr(metadata, field)``
          - ``"list[*].attr"``    → Iteration über Liste, Attribut extrahieren,
                                     Ergebnisse mit ", " verbinden
          - Verschachtelte Punkte wie ``"refresh_schedule.frequency"``
            → ``metadata.refresh_schedule.frequency``

        Gibt ``None`` zurück, wenn der Pfad nicht aufgelöst werden kann
        oder das Ergebnis leer ist.
        """
        if "[*]" in path:
            return self._resolve_list_path(metadata, path)
        return self._resolve_scalar_path(metadata, path)

    def _resolve_scalar_path(
        self, metadata: ReportMetadata, path: str
    ) -> str | None:
        """Löst einen skalaren Pfad wie ``report_name`` oder
        ``refresh_schedule.frequency`` auf."""
        parts = path.split(".")
        current: object = metadata

        for part in parts:
            if current is None:
                return None
            if not hasattr(current, part):
                return None
            current = getattr(current, part)

        if current is None:
            return None

        # Listen (z.B. times: list[str]) als Komma-getrennte Zeichenkette
        if isinstance(current, list):
            if not current:
                return None
            return ", ".join(str(item) for item in current)

        value = str(current)
        return value if value else None

    def _resolve_list_path(
        self, metadata: ReportMetadata, path: str
    ) -> str | None:
        """Löst einen Listen-Pfad wie ``measures[*].name`` auf.

        Extrahiert das Attribut aus jedem Listenelement und verbindet
        die Ergebnisse mit ``", "``.
        """
        # Pfad aufteilen: "data_sources[*].tables" → list_attr="data_sources", item_attr="tables"
        bracket_pos = path.index("[*]")
        list_attr = path[:bracket_pos]
        # Nach "[*]." kommt der Attributname
        item_attr = path[bracket_pos + 4:]  # len("[*].") == 4

        # Liste aus metadata holen
        if not hasattr(metadata, list_attr):
            return None
        items = getattr(metadata, list_attr)
        if not items or not isinstance(items, list):
            return None

        values: list[str] = []
        for item in items:
            val = self._extract_item_value(item, item_attr)
            if val is not None:
                values.append(val)

        return ", ".join(values) if values else None

    @staticmethod
    def _extract_item_value(item: object, attr: str) -> str | None:
        """Extrahiert einen Wert aus einem Listenelement.

        Unterstützt verschachtelte Attribute (z.B. ``"name"``) und
        dict/list-Werte, die als Zeichenkette formatiert werden.
        """
        parts = attr.split(".")
        current: object = item
        for part in parts:
            if current is None:
                return None
            if isinstance(current, dict):
                current = current.get(part)
            elif hasattr(current, part):
                current = getattr(current, part)
            else:
                return None

        if current is None:
            return None

        # dict-Werte (z.B. filter_expressions) als lesbare Zeichenkette
        if isinstance(current, dict):
            if not current:
                return None
            return "; ".join(f"{k}: {v}" for k, v in current.items())

        # list-Werte (z.B. tables: list[str]) als Komma-getrennte Zeichenkette
        if isinstance(current, list):
            if not current:
                return None
            return ", ".join(str(v) for v in current)

        value = str(current)
        return value if value else None
