"""Versions-Manager für die PBI Documentation by Oliver Gropp.

Verwaltet Versionierung, Änderungserkennung und Änderungsprotokoll
für bestehende Dokumentationen.
"""
from __future__ import annotations

import re
from datetime import date

from core.models import Change, ExistingDocInfo, VersionParseError

# Regex für die Metadaten-Tabelle (Kapitel 1)
_METADATA_FIELD_RE = re.compile(
    r"^\|\s*(?P<field>[^|]+?)\s*\|\s*(?P<value>[^|]*?)\s*\|$",
    re.MULTILINE,
)

# Regex für Changelog-Zeilen in Kapitel 10.2
_CHANGELOG_ROW_RE = re.compile(
    r"^\|\s*(?P<version>[^|]+?)\s*\|\s*(?P<datum>[^|]+?)\s*\|"
    r"\s*(?P<change>[^|]+?)\s*\|\s*(?P<typ>[^|]+?)\s*\|"
    r"\s*(?P<autor>[^|]+?)\s*\|$",
    re.MULTILINE,
)

# Regex für MAJOR.MINOR Versionsformat
_VERSION_RE = re.compile(r"^(\d+)\.(\d+)$")

# Felder, die als "fachlich" klassifiziert werden
_FACHLICH_FIELDS: set[str] = {
    "Kurzbeschreibung", "Fachbereich", "Konzerndomäne",
    "BusinessOwner", "TechOwner", "DataSteward", "Betrieb", "Kontakt",
    "BusinessZiel", "FachlicherKontext", "AbhängigkeitenOderKeine",
    "KPI1", "FachlicheLogik", "Begriff", "Definition", "Beschreibung",
    "DetailbeschreibungDatenquelle", "LinkOderReferenz", "Domäne",
    "BeschreibungOderKeine", "AppName",
    "RLSBeschreibung", "OLSBeschreibung", "LabelOderKeine", "Risiken",
    "MonitoringBeschreibung", "BekannteProbleme",
    "GitRepoOderAblage", "Kommentar", "Name", "Datum",
    "ReportName", "ProduktivDatum",
}


class VersionManager:
    """Verwaltet Versionierung und Änderungsprotokoll."""

    def parse_existing_doc(self, doc_path: str) -> ExistingDocInfo:
        """Liest bestehende Dokumentation und extrahiert Werte.

        Args:
            doc_path: Pfad zur bestehenden Markdown-Dokumentation.

        Returns:
            ExistingDocInfo mit Version, Werten, Changelog und Datum.

        Raises:
            VersionParseError: Wenn die Datei nicht gelesen oder geparst
                werden kann.
        """
        try:
            with open(doc_path, encoding="utf-8") as f:
                content = f.read()
        except OSError as exc:
            raise VersionParseError(
                f"Dokumentation kann nicht gelesen werden: {doc_path} – {exc}"
            ) from exc

        version = self._extract_metadata_field(content, "Aktuelle Version")
        last_updated = self._extract_metadata_field(
            content, "Letzte Aktualisierung"
        )
        values = self._extract_all_values(content)
        changelog = self._extract_changelog(content)

        return ExistingDocInfo(
            version=version,
            values=values,
            changelog=changelog,
            last_updated=last_updated,
        )

    def increment_version(self, current_version: str) -> str:
        """Inkrementiert die Versionsnummer im MAJOR.MINOR Format.

        Args:
            current_version: Aktuelle Version (z.B. "1.0").

        Returns:
            Neue Version mit inkrementiertem MINOR (z.B. "1.1").
            Bei leerem oder ungültigem Format wird "1.0" zurückgegeben.
        """
        if not current_version or not current_version.strip():
            return "1.0"

        match = _VERSION_RE.match(current_version.strip())
        if not match:
            return "1.0"

        major = int(match.group(1))
        minor = int(match.group(2))
        return f"{major}.{minor + 1}"

    def detect_changes(
        self,
        old_values: dict[str, str],
        new_values: dict[str, str],
    ) -> list[Change]:
        """Erkennt Änderungen zwischen alter und neuer Version.

        Args:
            old_values: Werte aus der bestehenden Dokumentation.
            new_values: Neue Werte für die Aktualisierung.

        Returns:
            Liste der erkannten Änderungen. Leer wenn identisch.
        """
        changes: list[Change] = []
        all_keys = set(old_values.keys()) | set(new_values.keys())

        for key in sorted(all_keys):
            old_val = old_values.get(key, "")
            new_val = new_values.get(key, "")
            if old_val != new_val:
                change_type = self._classify_change(key)
                changes.append(
                    Change(
                        field=key,
                        old_value=old_val,
                        new_value=new_val,
                        change_type=change_type,
                    )
                )

        return changes

    def create_changelog_entry(
        self,
        changes: list[Change],
        version: str,
        author: str,
    ) -> str:
        """Erstellt Changelog-Einträge als Markdown-Tabellenzeilen.

        Erzeugt pro Änderung eine eigene Zeile mit altem und neuem Wert.
        """
        today = date.today().isoformat()

        if not changes:
            return f"| {version} | {today} | Keine Änderungen | – | – | fachlich | {author} |"

        lines: list[str] = []
        for c in changes:
            # Feldname ohne Präfix
            field = c.field
            if "_" in field:
                parts = field.split("_", 1)
                if parts[0].isupper():
                    field = parts[1]

            old_short = self._sanitize_table_value((c.old_value or "–")[:40])
            new_short = self._sanitize_table_value((c.new_value or "–")[:40])
            lines.append(
                f"| {version} | {today} | {field} | {old_short} | {new_short} | {c.change_type} | {author} |"
            )

        return "\n".join(lines)

    def update_document(
        self,
        existing_doc_path: str,
        new_values: dict[str, str],
        author: str,
    ) -> str:
        """Aktualisiert eine bestehende Dokumentation.

        Liest das bestehende Dokument, erkennt Änderungen, inkrementiert
        die Version, fügt einen Changelog-Eintrag hinzu und aktualisiert
        die Metadaten.

        Args:
            existing_doc_path: Pfad zur bestehenden Dokumentation.
            new_values: Neue Werte für die Aktualisierung.
            author: Name des Autors.

        Returns:
            Der aktualisierte Dokumentinhalt als String.
        """
        existing_info = self.parse_existing_doc(existing_doc_path)
        changes = self.detect_changes(existing_info.values, new_values)
        new_version = self.increment_version(existing_info.version)

        try:
            with open(existing_doc_path, encoding="utf-8") as f:
                content = f.read()
        except OSError as exc:
            raise VersionParseError(
                f"Dokumentation kann nicht gelesen werden: "
                f"{existing_doc_path} – {exc}"
            ) from exc

        # Version aktualisieren
        content = self._update_metadata_field(
            content, "Aktuelle Version", new_version
        )

        # Letzte Aktualisierung auf heute setzen
        today = date.today().isoformat()
        content = self._update_metadata_field(
            content, "Letzte Aktualisierung", today
        )

        # Changelog-Eintrag hinzufügen
        if changes:
            entry = self.create_changelog_entry(changes, new_version, author)
            content = self._append_changelog_entry(content, entry)

        # Neue Werte in das Dokument einsetzen
        for key, value in new_values.items():
            old_val = existing_info.values.get(key, "")
            if old_val and old_val != value:
                content = content.replace(old_val, value, 1)

        return content

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

    @staticmethod
    def _sanitize_table_value(text: str) -> str:
        """Bereinigt einen Wert für Markdown-Tabellenzellen."""
        import re as _re
        text = text.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")
        text = text.replace("|", "∣")
        text = _re.sub(r"\*\*(.+?)\*\*", r"\1", text)
        text = _re.sub(r"\*(.+?)\*", r"\1", text)
        text = _re.sub(r"`(.+?)`", r"\1", text)
        text = _re.sub(r" {2,}", " ", text)
        return text.strip()

    @staticmethod
    def _extract_metadata_field(content: str, field_name: str) -> str:
        """Extrahiert einen Wert aus der Metadaten-Tabelle (Kapitel 1)."""
        for match in _METADATA_FIELD_RE.finditer(content):
            if match.group("field").strip() == field_name:
                value = match.group("value").strip()
                # Platzhalter ignorieren
                if value.startswith("{{") and value.endswith("}}"):
                    return ""
                return value
        return ""

    @staticmethod
    def _extract_all_values(content: str) -> dict[str, str]:
        """Extrahiert alle befüllten Werte aus dem Dokument.

        Sucht nach Tabellenfeldern und Abschnitts-Inhalten, die keine
        Platzhalter mehr sind.
        """
        values: dict[str, str] = {}
        for match in _METADATA_FIELD_RE.finditer(content):
            field_name = match.group("field").strip()
            value = match.group("value").strip()
            # Nur befüllte Felder (keine Platzhalter, keine Überschriften)
            if (
                value
                and not (value.startswith("{{") and value.endswith("}}"))
                and field_name not in ("Feld", "")
                and value not in ("Wert", "")
                and not all(c in "-" for c in field_name)
                and not all(c in "-" for c in value)
            ):
                values[field_name] = value
        return values

    @staticmethod
    def _extract_changelog(content: str) -> list[dict]:
        """Extrahiert Changelog-Einträge aus Kapitel 10.2."""
        changelog: list[dict] = []

        # Finde den Abschnitt 10.2
        section_match = re.search(
            r"###?\s*10\.2\s+Änderungsprotokoll(.*?)(?=\n##|\Z)",
            content,
            re.DOTALL,
        )
        if not section_match:
            return changelog

        section_content = section_match.group(1)

        for match in _CHANGELOG_ROW_RE.finditer(section_content):
            version = match.group("version").strip()
            datum = match.group("datum").strip()
            change = match.group("change").strip()
            typ = match.group("typ").strip()
            autor = match.group("autor").strip()

            # Überschriften- und Trennzeilen überspringen
            if version in ("Version", "---", "-------"):
                continue
            if all(c in "-" for c in version):
                continue

            changelog.append({
                "version": version,
                "datum": datum,
                "change": change,
                "typ": typ,
                "autor": autor,
            })

        return changelog

    @staticmethod
    def _classify_change(field_name: str) -> str:
        """Klassifiziert eine Änderung als 'fachlich' oder 'technisch'."""
        if field_name in _FACHLICH_FIELDS:
            return "fachlich"
        return "technisch"

    @staticmethod
    def _update_metadata_field(
        content: str, field_name: str, new_value: str
    ) -> str:
        """Aktualisiert ein Feld in der Metadaten-Tabelle."""
        pattern = re.compile(
            r"^(\|\s*" + re.escape(field_name) + r"\s*\|)\s*[^|]*?\s*(\|)$",
            re.MULTILINE,
        )
        replacement = rf"\g<1> {new_value} \g<2>"
        return pattern.sub(replacement, content)

    @staticmethod
    def _append_changelog_entry(content: str, entry: str) -> str:
        """Fügt einen neuen Changelog-Eintrag am Ende der Tabelle ein."""
        # Finde die letzte Zeile der Changelog-Tabelle in Kapitel 10.2
        section_match = re.search(
            r"(###?\s*10\.2\s+Änderungsprotokoll.*?)(\n---|\n##|\Z)",
            content,
            re.DOTALL,
        )
        if not section_match:
            return content

        section = section_match.group(1)

        # Finde die letzte Tabellenzeile
        table_lines = [
            line for line in section.split("\n")
            if line.strip().startswith("|") and line.strip().endswith("|")
        ]

        if table_lines:
            last_table_line = table_lines[-1]
            insert_pos = content.rfind(last_table_line) + len(last_table_line)
            content = (
                content[:insert_pos] + "\n" + entry + content[insert_pos:]
            )
        else:
            # Keine Tabelle gefunden – Eintrag nach Abschnittsüberschrift
            insert_pos = section_match.end(1)
            content = (
                content[:insert_pos] + "\n" + entry + content[insert_pos:]
            )

        return content
