"""
Generischer Script zum automatischen Generieren der API-Dokumentation.

Features:
- Auto-Discovery: Scannt automatisch alle Python-Module
- Convention-based: Nutzt Namenskonventionen für Standard-Docs
- YAML-Config: Optionale Overrides für spezielle Fälle
- Package-agnostic: Funktioniert mit jedem Python-Package

Usage:
    python scripts/generate_api_docs.py [--config path/to/config.yaml]
"""

from __future__ import annotations

import argparse
import ast
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path

try:
    import yaml
except ImportError:
    print("[ERROR] PyYAML nicht installiert. Bitte installieren: pip install pyyaml")
    sys.exit(1)


@dataclass
class ClassInfo:
    """Informationen über eine dokumentierte Klasse."""

    name: str
    module_path: str
    methods: list[str]
    is_dataclass: bool = False
    is_enum: bool = False
    docstring: str | None = None  # noqa: E501


@dataclass
class DocConfig:
    """Konfiguration für eine Dokumentationsdatei."""

    filename: str
    title: str
    classes: list[ClassInfo]
    heading_level: int = 2
    show_source: bool = False
    intro: str | None = None
    usage_example: str | None = None
    see_also: list[dict[str, str]] | None = None
    group_sections: list[dict[str, str | list[str]]] | None = None


@dataclass
class PackageConfig:
    """Package-spezifische Konfiguration."""

    name: str
    src_dir: Path
    docs_dir: Path
    auto_discovery_enabled: bool = True
    scan_dirs: list[str] = field(default_factory=lambda: ["."])
    exclude: list[str] = field(default_factory=list)
    preserve_files: list[str] = field(default_factory=lambda: ["index.md"])
    overrides: dict[str, dict[str, str | list[str]]] = field(default_factory=dict)
    grouping: dict[str, dict[str, list[dict[str, str]]]] = field(default_factory=dict)


class PythonClassExtractor:
    """Extrahiert Klassen und Methoden aus Python-Dateien via AST."""

    def extract_from_file(self, file_path: Path) -> list[ClassInfo]:
        """
        Extrahiert alle Klassen aus einer Python-Datei.

        Args:
            file_path: Pfad zur Python-Datei

        Returns:
            Liste von ClassInfo-Objekten
        """
        try:
            with open(file_path, encoding="utf-8") as f:
                source = f.read()
        except Exception as e:
            print(f"[WARN] Konnte {file_path} nicht lesen: {e}")
            return []

        try:
            tree = ast.parse(source)
        except SyntaxError as e:
            print(f"[WARN] Syntax-Fehler in {file_path}: {e}")
            return []

        classes = []
        for node in ast.walk(tree):
            if isinstance(node, ast.ClassDef):
                class_info = self._extract_class_info(node, file_path)
                if class_info:
                    classes.append(class_info)

        return classes

    def _extract_class_info(
        self, node: ast.ClassDef, file_path: Path
    ) -> ClassInfo | None:  # noqa: E501
        """
        Extrahiert Informationen über eine Klasse.

        Args:
            node: AST ClassDef Node
            file_path: Pfad zur Datei

        Returns:
            ClassInfo oder None wenn Klasse privat ist
        """
        # Überspringe private Klassen
        if node.name.startswith("_"):
            return None

        # Ermittle Modulpfad
        module_path = self._get_module_path(file_path)
        full_module_path = f"{module_path}.{node.name}"

        # Extrahiere Methoden
        methods = []
        for item in node.body:
            if isinstance(item, ast.FunctionDef) and not item.name.startswith("_"):
                methods.append(item.name)
            elif isinstance(item, ast.FunctionDef) and item.name == "__init__":
                methods.insert(0, "__init__")

        # Prüfe ob Dataclass
        is_dataclass = any(
            isinstance(dec, ast.Name) and dec.id == "dataclass"
            for dec in node.decorator_list
        )

        # Prüfe ob Enum
        is_enum = any(
            (isinstance(base, ast.Name) and base.id == "Enum")
            or (isinstance(base, ast.Attribute) and base.attr == "Enum")
            for base in node.bases
        )

        # Extrahiere Docstring
        docstring = ast.get_docstring(node)

        return ClassInfo(
            name=node.name,
            module_path=full_module_path,
            methods=methods,
            is_dataclass=is_dataclass,
            is_enum=is_enum,
            docstring=docstring,
        )

    def _get_module_path(self, file_path: Path) -> str:
        """
        Konvertiert Dateipfad zu Python-Modulpfad.

        Args:
            file_path: Pfad zur Datei

        Returns:
            Modulpfad (z.B. package.module)
        """
        parts = file_path.parts
        try:
            src_idx = parts.index("src")
            module_parts = parts[src_idx + 1 :]
        except ValueError:
            module_parts = parts

        module_parts = list(module_parts)
        if module_parts[-1].endswith(".py"):
            module_parts[-1] = module_parts[-1][:-3]

        if module_parts[-1] == "__init__":
            module_parts = module_parts[:-1]

        return ".".join(module_parts)


class MarkdownGenerator:
    """Generiert Markdown-Dateien für die API-Dokumentation."""

    def generate(self, config: DocConfig, output_path: Path) -> None:
        """
        Generiert eine Markdown-Datei aus DocConfig.

        Args:
            config: Dokumentations-Konfiguration
            output_path: Ausgabepfad für die .md Datei
        """
        lines = []

        # Titel
        lines.append(f"# {config.title}")
        lines.append("")

        # Intro
        if config.intro:
            lines.append(config.intro)
            lines.append("")

        # Klassen dokumentieren
        if config.group_sections:
            # Mit Sections gruppiert
            for section in config.group_sections:
                section_title = section.get("title", "")
                class_names = section.get("classes", [])

                lines.append(f"## {section_title}")
                lines.append("")

                for cls in config.classes:
                    if cls.name in class_names:
                        self._add_class_doc(cls, config, lines)
        else:
            # Ohne Grouping
            for cls in config.classes:
                self._add_class_doc(cls, config, lines)

        # Schreibe Datei
        output_path.parent.mkdir(parents=True, exist_ok=True)
        with open(output_path, "w", encoding="utf-8") as f:
            f.write("\n".join(lines))

    def _add_class_doc(
        self, cls: ClassInfo, config: DocConfig, lines: list[str]
    ) -> None:
        """Fügt Dokumentation für eine Klasse hinzu."""
        lines.append(f"::: {cls.module_path}")
        lines.append("    options:")
        lines.append(f"      show_source: {str(config.show_source).lower()}")
        lines.append(f"      heading_level: {config.heading_level}")

        # Members-Logik:
        # - Enums: Keine members (zeigt Enum-Werte automatisch)
        # - Dataclasses: show_docstring_attributes (zeigt Attribute automatisch)
        # - Reguläre Klassen: Keine members-Liste (verwendet members_order aus mkdocs.yml)

        if cls.is_enum:
            # Enums: keine members
            pass
        elif cls.is_dataclass:
            # Dataclasses: zeige Attribute
            lines.append("      show_docstring_attributes: true")
            # Keine explizite members-Liste mehr, damit members_order greift

        lines.append("")

    def _format_docstring_summary(self, docstring: str) -> str:
        """Extrahiert Zusammenfassung aus Docstring."""
        lines = docstring.strip().split("\n")
        summary_lines = []
        for line in lines:
            if not line.strip() and summary_lines:
                break
            if line.strip():
                summary_lines.append(line.strip())
        return " ".join(summary_lines) if summary_lines else docstring.strip()


class AutoDiscovery:
    """Auto-Discovery für Python-Module."""

    def __init__(self, package_config: PackageConfig, extractor: PythonClassExtractor):
        self.config = package_config
        self.extractor = extractor

    def discover_modules(self) -> dict[str, list[ClassInfo]]:  # noqa: E501
        """
        Scannt alle konfigurierten Verzeichnisse und entdeckt Module.

        Returns:
            Dict: {module_path: [ClassInfo]}
        """
        discovered = {}

        for scan_dir in self.config.scan_dirs:
            base_path = self.config.src_dir / self.config.name / scan_dir
            if not base_path.exists():
                continue

            # Finde alle .py Dateien
            if scan_dir == ".":
                # Root-Level: Nur direkte .py Dateien
                py_files = [
                    f for f in base_path.glob("*.py") if f.name != "__init__.py"
                ]
            else:
                # Sub-Directory: Alle .py Dateien
                py_files = [
                    f for f in base_path.glob("**/*.py") if f.name != "__init__.py"
                ]

            for py_file in py_files:
                # Prüfe Exclude-Liste
                if self._is_excluded(py_file):
                    continue

                classes = self.extractor.extract_from_file(py_file)
                if classes:
                    relative_path = py_file.relative_to(
                        self.config.src_dir / self.config.name
                    )
                    discovered[str(relative_path)] = classes

        return discovered

    def _is_excluded(self, py_file: Path) -> bool:
        """Prüft ob Datei in Exclude-Liste ist."""
        for exclude_pattern in self.config.exclude:
            if py_file.name == exclude_pattern or str(py_file).endswith(
                exclude_pattern
            ):
                return True
        return False

    def build_doc_configs(
        self, discovered: dict[str, list[ClassInfo]]
    ) -> list[DocConfig]:  # noqa: E501
        """
        Erstellt DocConfigs aus discovered Modulen.

        Args:
            discovered: Dict von {module_path: [ClassInfo]}

        Returns:
            Liste von DocConfig
        """
        configs = []
        processed_modules = set()

        # 1. Prüfe Overrides mit expliziten `modules` Listen (Multi-Module Docs)
        for override_filename, override_data in self.config.overrides.items():
            if "modules" in override_data:
                # Multi-Module Doc
                module_list = override_data["modules"]
                combined_classes = []

                for module_path in module_list:
                    # Normalisiere Pfad (operations/rest_file.py → operations\rest_file.py auf Windows)
                    norm_path = str(Path(module_path))
                    if norm_path in discovered:
                        combined_classes.extend(discovered[norm_path])
                        processed_modules.add(norm_path)

                if combined_classes:
                    configs.append(
                        DocConfig(
                            filename=override_filename,
                            title=override_data.get("title", "Multi-Module Doc"),
                            classes=combined_classes,
                            intro=override_data.get("intro"),
                            usage_example=override_data.get("usage_example"),
                            see_also=override_data.get("see_also"),
                            heading_level=override_data.get("heading_level", 2),
                            group_sections=override_data.get("group_sections"),
                        )
                    )

        # 2. Gruppiere verbleibende Module nach Kategorien
        operations = {}
        models = {}
        root_modules = {}

        for module_path, classes in discovered.items():
            if module_path in processed_modules:
                continue

            if module_path.startswith("operations"):
                operations[module_path] = classes
            elif module_path.startswith("models"):
                models[module_path] = classes
            else:
                root_modules[module_path] = classes

        # 3. Root-Level Module (client.py, highlevel_client.py)
        for module_path, classes in root_modules.items():
            filename = self._module_to_filename(module_path)
            override = self.config.overrides.get(filename, {})

            configs.append(
                DocConfig(
                    filename=filename,
                    title=override.get("title", self._module_to_title(module_path)),
                    classes=classes,
                    intro=override.get("intro"),
                    usage_example=override.get("usage_example"),
                    see_also=override.get("see_also"),
                )
            )

        # 4. Operations Module (einzeln, wenn nicht bereits in Multi-Module Doc)
        for module_path, classes in operations.items():
            filename = self._module_to_filename(module_path)
            override = self.config.overrides.get(filename, {})

            configs.append(
                DocConfig(
                    filename=filename,
                    title=override.get("title", self._module_to_title(module_path)),
                    classes=classes,
                    intro=override.get("intro"),
                    usage_example=override.get("usage_example"),
                    see_also=override.get("see_also"),
                    heading_level=override.get("heading_level", 2),
                    group_sections=override.get("group_sections"),
                )
            )

        # 5. Models: Gruppierung nach Kategorien
        model_groups = self._group_models(models)
        for group_name, group_classes in model_groups.items():
            filename = f"models-{group_name}.md"
            override = self.config.overrides.get(filename, {})

            configs.append(
                DocConfig(
                    filename=filename,
                    title=override.get("title", f"{group_name.title()} Models"),
                    classes=group_classes,
                    intro=override.get(
                        "intro", f"Dataclasses für {group_name.title()}."
                    ),
                    usage_example=override.get("usage_example"),
                    see_also=override.get("see_also"),
                )
            )

        return configs

    def _group_models(
        self, models: dict[str, list[ClassInfo]]
    ) -> dict[str, list[ClassInfo]]:  # noqa: E501
        """
        Gruppiert Model-Module nach Kategorien.

        Args:
            models: Dict von {module_path: [ClassInfo]}

        Returns:
            Dict von {category: [ClassInfo]}
        """
        grouping_config = self.config.grouping.get("models", {})
        categories = grouping_config.get("categories", [])

        groups = {}

        if not categories:
            # Fallback: Jedes Modul als eigene Kategorie
            for module_path, classes in models.items():
                category = Path(module_path).stem
                groups[category] = classes
            return groups

        # Gruppierung nach Kategorien
        for category in categories:
            pattern = category.get("pattern", "")
            # Verwende filename aus Config wenn vorhanden, sonst generiere aus name
            filename = category.get("filename", "")
            if filename:
                # Entferne "models-" Präfix und ".md" Extension
                name_key = filename.replace("models-", "").replace(".md", "")
            else:
                name_key = (
                    category.get("name", "")
                    .lower()
                    .replace(" models", "")
                    .replace(" ", "-")
                )

            matching_classes = []
            for module_path, classes in models.items():
                module_name = Path(module_path).stem
                if re.search(pattern, module_name):
                    matching_classes.extend(classes)

            if matching_classes:
                groups[name_key] = matching_classes

        return groups

    def _module_to_filename(self, module_path: str) -> str:
        """Konvertiert Modulpfad zu Dateiname."""
        # operations/queries.py -> queries.md
        # client.py -> client.md
        # highlevel_client.py -> highlevel-client.md
        stem = Path(module_path).stem
        return stem.replace("_", "-") + ".md"

    def _module_to_title(self, module_path: str) -> str:
        """Konvertiert Modulpfad zu Titel."""
        stem = Path(module_path).stem
        # highlevel_client -> HighLevel Client
        # queries -> Queries
        words = stem.replace("_", " ").split()
        return " ".join(word.capitalize() for word in words)


class ApiDocGenerator:
    """Hauptklasse für die Generierung der API-Dokumentation."""

    def __init__(self, config_path: Path | None = None) -> None:
        """
        Initialisiert den Generator.

        Args:
            config_path: Pfad zur YAML-Config (optional)
        """
        self.config_path = config_path or Path("scripts/api_docs_config.yaml")
        self.package_config = self._load_config()
        self.extractor = PythonClassExtractor()
        self.generator = MarkdownGenerator()
        self.discovery = AutoDiscovery(self.package_config, self.extractor)

    def _load_config(self) -> PackageConfig:
        """Lädt Konfiguration aus YAML."""
        if not self.config_path.exists():
            print(f"[ERROR] Config nicht gefunden: {self.config_path}")
            sys.exit(1)

        with open(self.config_path, encoding="utf-8") as f:
            config_data = yaml.safe_load(f)

        package_data = config_data.get("package", {})
        auto_discovery_data = config_data.get("auto_discovery", {})
        overrides = config_data.get("overrides", {})
        grouping = auto_discovery_data.get("grouping", {})

        # Basis-Pfad relativ zur Config
        base_path = self.config_path.parent.parent
        src_dir = base_path / package_data.get("src_dir", "src")
        docs_dir = base_path / package_data.get("docs_dir", "docs/api")

        return PackageConfig(
            name=package_data.get("name", ""),
            src_dir=src_dir,
            docs_dir=docs_dir,
            auto_discovery_enabled=auto_discovery_data.get("enabled", True),
            scan_dirs=auto_discovery_data.get("scan_dirs", ["."]),
            exclude=auto_discovery_data.get("exclude", []),
            preserve_files=config_data.get("preserve_files", ["index.md"]),
            overrides=overrides,
            grouping=grouping,
        )

    def generate_all(self) -> None:
        """Generiert alle API-Dokumentationsdateien."""
        print("=> Generiere API-Dokumentation...")
        print(f"   Package: {self.package_config.name}")
        print(f"   Src: {self.package_config.src_dir}")
        print(f"   Docs: {self.package_config.docs_dir}")
        print()

        # Auto-Discovery
        if self.package_config.auto_discovery_enabled:
            print("[1/3] Auto-Discovery...")
            discovered = self.discovery.discover_modules()
            print(f"      Gefunden: {len(discovered)} Module")

            doc_configs = self.discovery.build_doc_configs(discovered)
            print(f"      Erstellt: {len(doc_configs)} Doc-Configs")
        else:
            print("[SKIP] Auto-Discovery deaktiviert")
            doc_configs = []

        # Lösche alte Dateien
        print("\n[2/3] Bereinige alte Dateien...")
        self._clean_old_docs()

        # Generiere neue Dateien
        print("\n[3/3] Generiere Dokumentation...")
        generated_files = []
        for config in doc_configs:
            output_path = self.package_config.docs_dir / config.filename
            self.generator.generate(config, output_path)
            generated_files.append(config.filename)
            print(f"      [OK] {config.filename}")

        print(f"\n==> {len(generated_files)} Dateien generiert!")

    def _clean_old_docs(self) -> None:
        """Löscht alte generierte Dokumentationsdateien."""
        if not self.package_config.docs_dir.exists():
            return

        for md_file in self.package_config.docs_dir.glob("*.md"):
            if md_file.name in self.package_config.preserve_files:
                continue
            md_file.unlink()
            print(f"      [DEL] {md_file.name}")


def main() -> None:
    """Hauptfunktion."""
    parser = argparse.ArgumentParser(
        description="Generiert API-Dokumentation aus Python-Code"
    )
    parser.add_argument(
        "--config",
        type=Path,
        help="Pfad zur YAML-Config (default: scripts/api_docs_config.yaml)",
    )
    args = parser.parse_args()

    try:
        generator = ApiDocGenerator(config_path=args.config)
        generator.generate_all()
    except Exception as e:
        print(f"[ERROR] {e}")
        import traceback

        traceback.print_exc()
        sys.exit(1)


if __name__ == "__main__":
    main()
