"""Orchestrierung der spezifischen Einzel-Dokumentation von Fabric-Elementen.

Aktuell werden **Dataflows** (Gen1 & Gen2), **Fabric Notebooks**, **Fabric Copy
Jobs**, **Fabric Data Pipelines**, **Fabric Lakehouses**, **Fabric Warehouses**,
**Fabric Eventhouses** und **Power BI / Fabric Dashboards** sowie **Fabric
KQL-Datenbanken**, **SQL Databases**, **Environments** und **Mirrored
Databases** und **Eventstreams** sowie **Spark Job Definitions**, **KQL-Querysets** und **Reflex-Items** sowie **API-for-GraphQL-Items** unterstützt; die
Struktur ist so angelegt, dass weitere Elementtypen später ergänzt werden können.

Zusätzlich zur Erst-Erzeugung wird ein **Dokumentations-Update** unterstützt:
Ist bereits eine Dokumentation vorhanden, werden die automatisch extrahierten
Abschnitte (Metadaten, Entitäten, Power Query …) neu eingelesen, während die
manuell gepflegten Abschnitte (Ansprechpartner, Business-Ziel, Betriebshinweise
usw.) aus dem Bestandsdokument übernommen werden. Version und Änderungsprotokoll
werden dabei fortgeschrieben.
"""
from __future__ import annotations

import re
from dataclasses import dataclass
from datetime import date

from core import copyjob_doc_generator as cjdoc
from core import dashboard_doc_generator as dbdoc
from core import dataflow_doc_generator as dfdoc
from core import environment_doc_generator as envdoc
from core import eventhouse_doc_generator as ehdoc
from core import eventstream_doc_generator as esdoc
from core import graphqlapi_doc_generator as gqdoc
from core import kqldatabase_doc_generator as kqldoc
from core import kqlqueryset_doc_generator as qsdoc
from core import lakehouse_doc_generator as lhdoc
from core import mirroreddb_doc_generator as mddoc
from core import notebook_doc_generator as nbdoc
from core import pipeline_doc_generator as pldoc
from core import reflex_doc_generator as rxdoc
from core import sparkjob_doc_generator as sjdoc
from core import sqldatabase_doc_generator as sqdoc
from core import warehouse_doc_generator as whdoc
from core.models import (
    CopyJobInfo,
    DashboardInfo,
    DataflowInfo,
    DataPipelineInfo,
    EnvironmentInfo,
    EventhouseInfo,
    EventstreamInfo,
    GraphQlApiInfo,
    KqlDatabaseInfo,
    KqlQuerysetInfo,
    LakehouseInfo,
    MirroredDatabaseInfo,
    NotebookInfo,
    ReflexInfo,
    SparkJobDefinitionInfo,
    SqlDatabaseInfo,
    WarehouseInfo,
)

# Unterstützte Elementtypen (Reihenfolge = Anzeige-Reihenfolge im Frontend).
SUPPORTED_ELEMENT_TYPES = [
    "Dataflow", "Notebook", "CopyJob", "DataPipeline", "Lakehouse", "Warehouse",
    "Eventhouse", "Dashboard", "KQLDatabase", "SQLDatabase", "Environment",
    "MirroredDatabase",
    "Eventstream", "SparkJobDefinition", "KQLQueryset", "Reflex", "GraphQLApi",
]

# ---------------------------------------------------------------------------
# Abschnitts-basiertes Merge (sprachunabhängig über die Kapitelnummern)
# ---------------------------------------------------------------------------

# Überschriften der Form "## 1. ..." oder "### 6.3 ..." – wir gruppieren über
# die führende Nummer, die in der DE- und EN-Vorlage identisch ist.
_HEADING_RE = re.compile(r"^(#{2,3})[ \t]+(\d+(?:\.\d+)?)\b[^\n]*$", re.MULTILINE)

# Kapitel, deren Inhalt manuell gepflegt wird und beim Update erhalten bleibt.
_MANUAL_SECTIONS = frozenset(
    {
        "2",  # Ansprechpartner
        "3.1", "3.2", "3.3",  # Business-Ziel
        "5.2",  # Detailbeschreibung Datenquellen
        "6.3", "6.4", "6.5",  # Parameter, Fehlerbehandlung, Performance
        "7.2", "7.3",  # Monitoring, bekannte Probleme
        "8.1", "8.2", "8.3",  # Sicherheit
        "9.1",  # Repository
        "10",  # Freigabe & Review
    }
)
_METADATA_SECTION = "1"
_CHANGELOG_SECTION = "9.2"

# Locale-unabhängige Label-Mengen für die Metadaten-Tabelle.
_LABELS_MANUAL_META = frozenset({"Fachbereich", "Business Unit", "Konzerndomäne", "Domain"})
_LABELS_VERSION = frozenset({"Aktuelle Version", "Current Version"})
_LABELS_UPDATED = frozenset({"Letzte Aktualisierung", "Last Updated"})

_VERSION_RE = re.compile(r"^(\d+)\.(\d+)$")


@dataclass
class ElementDocResult:
    """Ergebnis einer Element-Dokumentation."""
    markdown: str
    element_type: str
    generation: str
    entity_count: int
    changes_count: int
    version: str
    is_update: bool
    name: str = ""


def _is_placeholder_or_empty(value: str) -> bool:
    """True, wenn der Wert leer, ein Hinweis (_…_) oder ein Platzhalter ist."""
    v = (value or "").strip()
    if not v or v in ("–", "-"):
        return True
    if v.startswith("_") and v.endswith("_"):
        return True
    if v.startswith("{{") and v.endswith("}}"):
        return True
    return False


def _increment_version(current: str) -> str:
    """Erhöht MINOR im Format MAJOR.MINOR; Fallback 1.1."""
    m = _VERSION_RE.match((current or "").strip())
    if not m:
        return "1.1"
    return f"{int(m.group(1))}.{int(m.group(2)) + 1}"


def _split_sections(md: str) -> tuple[str, list[tuple[str, str]]]:
    """Zerlegt ein Markdown-Dokument in (Präambel, [(Nummer, Blocktext), …]).

    Der Blocktext enthält die Überschriftszeile und alles bis zur nächsten
    Überschrift (exklusive).
    """
    matches = list(_HEADING_RE.finditer(md))
    if not matches:
        return md, []
    preamble = md[: matches[0].start()]
    sections: list[tuple[str, str]] = []
    for idx, match in enumerate(matches):
        number = match.group(2)
        start = match.start()
        end = matches[idx + 1].start() if idx + 1 < len(matches) else len(md)
        sections.append((number, md[start:end]))
    return preamble, sections


def _parse_metadata_rows(section_text: str) -> dict[str, str]:
    """Liest die ``| Label | Wert |``-Zeilen einer Metadaten-Sektion."""
    rows: dict[str, str] = {}
    for line in section_text.splitlines():
        stripped = line.strip()
        if not (stripped.startswith("|") and stripped.endswith("|")):
            continue
        cells = [c.strip() for c in stripped.strip("|").split("|")]
        if len(cells) == 2:
            rows[cells[0]] = cells[1]
    return rows


def _merge_metadata_section(
    new_text: str, old_rows: dict[str, str], new_version: str, today: str
) -> str:
    """Übernimmt manuell gepflegte Metadaten-Zeilen und setzt Version/Datum."""
    out_lines: list[str] = []
    for line in new_text.splitlines():
        stripped = line.strip()
        if stripped.startswith("|") and stripped.endswith("|"):
            cells = [c.strip() for c in stripped.strip("|").split("|")]
            if len(cells) == 2:
                label, value = cells
                if label in _LABELS_VERSION:
                    value = new_version
                elif label in _LABELS_UPDATED:
                    value = today
                elif label in _LABELS_MANUAL_META:
                    old_val = old_rows.get(label, "")
                    if not _is_placeholder_or_empty(old_val):
                        value = old_val
                out_lines.append(f"| {label} | {value} |")
                continue
        out_lines.append(line)
    return "\n".join(out_lines)


def _append_changelog_row(new_text: str, old_text: str, row: str) -> str:
    """Baut die Changelog-Sektion: alte Historie + neue Zeile.

    Bevorzugt die (ggf. gewachsene) Tabelle aus dem Bestandsdokument und hängt
    die neue Zeile an; fällt auf die neue Sektion zurück, falls keine Tabelle
    gefunden wird.
    """
    base = old_text if old_text and "|" in old_text else new_text
    lines = base.rstrip().splitlines()
    # Letzte Tabellenzeile finden
    last_idx = -1
    for i, line in enumerate(lines):
        s = line.strip()
        if s.startswith("|") and s.endswith("|"):
            last_idx = i
    if last_idx == -1:
        return base.rstrip() + "\n" + row + "\n\n"
    lines.insert(last_idx + 1, row)
    return "\n".join(lines) + "\n\n"


def merge_dataflow_markdown(
    old_md: str,
    new_md: str,
    *,
    language: str = "de",
    author: str = "Auto-Dokumentation",
) -> tuple[str, int]:
    """Führt Bestands- und Neu-Dokumentation zusammen (Update).

    Automatische Abschnitte werden aus ``new_md`` übernommen, manuelle
    Abschnitte aus ``old_md``. Version wird erhöht, ein Changelog-Eintrag
    angehängt.

    Returns:
        (zusammengeführtes Markdown, Anzahl geänderter Auto-Abschnitte).
    """
    _old_pre, old_sections = _split_sections(old_md)
    new_pre, new_sections = _split_sections(new_md)
    old_map = {num: text for num, text in old_sections}

    old_meta_rows = _parse_metadata_rows(old_map.get(_METADATA_SECTION, ""))
    old_version = ""
    for label in _LABELS_VERSION:
        if label in old_meta_rows:
            old_version = old_meta_rows[label]
            break
    new_version = _increment_version(old_version)
    today = date.today().isoformat()

    changes = 0
    rebuilt: list[str] = []
    for number, new_text in new_sections:
        old_text = old_map.get(number, "")
        if number == _METADATA_SECTION:
            rebuilt.append(
                _merge_metadata_section(new_text, old_meta_rows, new_version, today)
            )
        elif number == _CHANGELOG_SECTION:
            row = _changelog_row(new_version, today, author, language)
            rebuilt.append(_append_changelog_row(new_text, old_text, row))
        elif number in _MANUAL_SECTIONS and old_text:
            rebuilt.append(old_text)
        else:
            # Automatischer Abschnitt: neue Fassung übernehmen und Änderung zählen
            if old_text and old_text.strip() != new_text.strip():
                changes += 1
            rebuilt.append(new_text)

    merged = new_pre + "".join(rebuilt)
    return merged, changes


def _changelog_row(version: str, today: str, author: str, language: str) -> str:
    """Baut eine Changelog-Tabellenzeile (5 Spalten wie in der Vorlage)."""
    if (language or "de").lower().startswith("en"):
        change = "Automatic update (structure/transformations re-read)"
        ctype = "technical"
    else:
        change = "Automatische Aktualisierung (Struktur/Transformationen neu eingelesen)"
        ctype = "technisch"
    return f"| {version} | {today} | {change} | {ctype} | {author} |"


# ---------------------------------------------------------------------------
# Öffentliche Orchestrierung
# ---------------------------------------------------------------------------

def document_element(
    connector,  # type: ignore[no-untyped-def] – IConnector mit extract_dataflow
    element_type: str,
    element_id: str,
    *,
    generation: str = "Gen1",
    template_content: str,
    export_date: str,
    language: str = "de",
    name: str = "",
    description: str = "",
    web_url: str = "",
    existing_content: str | None = None,
) -> ElementDocResult:
    """Extrahiert ein Fabric-Element und rendert seine Dokumentation.

    Args:
        connector: Verbundener FabricConnector (mit ``extract_dataflow`` bzw.
            ``extract_notebook``).
        element_type: Elementtyp, ``"Dataflow"`` oder ``"Notebook"``.
        element_id: Objekt-/Item-ID des Elements.
        generation: "Gen1"/"Gen2" (nur relevant für Dataflows).
        template_content: Inhalt der zum Elementtyp passenden Vorlage.
        export_date: Datum für das Dokument (ISO-String).
        language: "de"/"en".
        name/description/web_url: Aus der Enumeration bekannte Basiswerte.
        existing_content: Inhalt einer Bestandsdokumentation für ein Update
            (oder None für Erst-Erzeugung).

    Returns:
        ElementDocResult mit gerendertem Markdown und Metainformationen.
    """
    if element_type not in SUPPORTED_ELEMENT_TYPES:
        raise ValueError(f"Nicht unterstützter Elementtyp: {element_type}")

    if element_type == "Notebook":
        nb: NotebookInfo = connector.extract_notebook(
            element_id,
            name=name,
            description=description,
            web_url=web_url,
        )
        item_count = len(nb.cells)
        result_generation = "–"
        new_md = nbdoc.render_notebook(template_content, nb, export_date, language)
        result_name = nb.name
    elif element_type == "CopyJob":
        cj: CopyJobInfo = connector.extract_copyjob(
            element_id,
            name=name,
            description=description,
            web_url=web_url,
        )
        item_count = len(cj.activities)
        result_generation = "–"
        new_md = cjdoc.render_copyjob(template_content, cj, export_date, language)
        result_name = cj.name
    elif element_type == "DataPipeline":
        pl: DataPipelineInfo = connector.extract_datapipeline(
            element_id,
            name=name,
            description=description,
            web_url=web_url,
        )
        item_count = len(pl.activities)
        result_generation = "–"
        new_md = pldoc.render_pipeline(template_content, pl, export_date, language)
        result_name = pl.name
    elif element_type == "Lakehouse":
        lh: LakehouseInfo = connector.extract_lakehouse(
            element_id,
            name=name,
            description=description,
            web_url=web_url,
        )
        item_count = len(lh.tables)
        result_generation = "–"
        new_md = lhdoc.render_lakehouse(template_content, lh, export_date, language)
        result_name = lh.name
    elif element_type == "Warehouse":
        wh: WarehouseInfo = connector.extract_warehouse(
            element_id,
            name=name,
            description=description,
            web_url=web_url,
        )
        item_count = 0
        result_generation = "–"
        new_md = whdoc.render_warehouse(template_content, wh, export_date, language)
        result_name = wh.name
    elif element_type == "Eventhouse":
        eh: EventhouseInfo = connector.extract_eventhouse(
            element_id,
            name=name,
            description=description,
            web_url=web_url,
        )
        item_count = len(eh.databases)
        result_generation = "–"
        new_md = ehdoc.render_eventhouse(template_content, eh, export_date, language)
        result_name = eh.name
    elif element_type == "Dashboard":
        db: DashboardInfo = connector.extract_dashboard(
            element_id,
            name=name,
            description=description,
            web_url=web_url,
        )
        item_count = len(db.tiles)
        result_generation = "–"
        new_md = dbdoc.render_dashboard(template_content, db, export_date, language)
        result_name = db.name
    elif element_type == "KQLDatabase":
        kh: KqlDatabaseInfo = connector.extract_kqldatabase(
            element_id,
            name=name,
            description=description,
            web_url=web_url,
        )
        item_count = 0
        result_generation = "–"
        new_md = kqldoc.render_kqldatabase(
            template_content, kh, export_date, language
        )
        result_name = kh.name
    elif element_type == "SQLDatabase":
        sq: SqlDatabaseInfo = connector.extract_sqldatabase(
            element_id,
            name=name,
            description=description,
            web_url=web_url,
        )
        item_count = 0
        result_generation = "–"
        new_md = sqdoc.render_sqldatabase(
            template_content, sq, export_date, language
        )
        result_name = sq.name
    elif element_type == "Environment":
        env: EnvironmentInfo = connector.extract_environment(
            element_id,
            name=name,
            description=description,
            web_url=web_url,
        )
        item_count = len(env.libraries)
        result_generation = "–"
        new_md = envdoc.render_environment(
            template_content, env, export_date, language
        )
        result_name = env.name
    elif element_type == "MirroredDatabase":
        md: MirroredDatabaseInfo = connector.extract_mirroreddb(
            element_id,
            name=name,
            description=description,
            web_url=web_url,
        )
        item_count = len(md.mirrored_tables)
        result_generation = "–"
        new_md = mddoc.render_mirroreddb(
            template_content, md, export_date, language
        )
        result_name = md.name
    elif element_type == "Eventstream":
        es: EventstreamInfo = connector.extract_eventstream(
            element_id,
            name=name,
            description=description,
            web_url=web_url,
        )
        item_count = len(es.sources) + len(es.destinations) + len(es.operators)
        result_generation = "–"
        new_md = esdoc.render_eventstream(
            template_content, es, export_date, language
        )
        result_name = es.name
    elif element_type == "SparkJobDefinition":
        sj: SparkJobDefinitionInfo = connector.extract_sparkjob(
            element_id,
            name=name,
            description=description,
            web_url=web_url,
        )
        item_count = 0
        result_generation = "–"
        new_md = sjdoc.render_sparkjob(
            template_content, sj, export_date, language
        )
        result_name = sj.name
    elif element_type == "KQLQueryset":
        qs: KqlQuerysetInfo = connector.extract_kqlqueryset(
            element_id,
            name=name,
            description=description,
            web_url=web_url,
        )
        item_count = len(qs.queries)
        result_generation = "–"
        new_md = qsdoc.render_kqlqueryset(
            template_content, qs, export_date, language
        )
        result_name = qs.name
    elif element_type == "Reflex":
        rx: ReflexInfo = connector.extract_reflex(
            element_id,
            name=name,
            description=description,
            web_url=web_url,
        )
        item_count = len(rx.rules)
        result_generation = "–"
        new_md = rxdoc.render_reflex(
            template_content, rx, export_date, language
        )
        result_name = rx.name
    elif element_type == "GraphQLApi":
        gq: GraphQlApiInfo = connector.extract_graphqlapi(
            element_id,
            name=name,
            description=description,
            web_url=web_url,
        )
        item_count = len(gq.data_sources)
        result_generation = "–"
        new_md = gqdoc.render_graphqlapi(
            template_content, gq, export_date, language
        )
        result_name = gq.name
    else:  # "Dataflow"
        df: DataflowInfo = connector.extract_dataflow(
            element_id,
            generation,
            name=name,
            description=description,
            web_url=web_url,
        )
        item_count = len(df.entities)
        result_generation = df.generation
        new_md = dfdoc.render_dataflow(template_content, df, export_date, language)
        result_name = df.name

    if existing_content:
        merged, changes = merge_dataflow_markdown(
            existing_content, new_md, language=language
        )
        # Version aus dem zusammengeführten Dokument ablesen (für die Rückgabe)
        _pre, sections = _split_sections(merged)
        version = ""
        for num, text in sections:
            if num == _METADATA_SECTION:
                rows = _parse_metadata_rows(text)
                for label in _LABELS_VERSION:
                    if label in rows:
                        version = rows[label]
                        break
                break
        return ElementDocResult(
            markdown=merged,
            element_type=element_type,
            generation=result_generation,
            entity_count=item_count,
            changes_count=changes,
            version=version or "1.1",
            is_update=True,
            name=result_name,
        )

    return ElementDocResult(
        markdown=new_md,
        element_type=element_type,
        generation=result_generation,
        entity_count=item_count,
        changes_count=0,
        version="1.0",
        is_update=False,
        name=result_name,
    )
