"""Orchestrierung des Fabric-Workspace-Bulk-Exports.

Der :class:`WorkspaceExporter` bündelt den gesamten Ablauf:

1. Enumeration des Workspaces (Berichte, Semantic Models, sonstige Elemente,
   Zugriffsberechtigungen).
2. Planung der Dateistruktur (je Semantic Model eine Datei; bei genau einem
   zugehörigen Bericht wird dieser inline dokumentiert, bei mehreren erhält
   jeder Bericht eine eigene Datei mit Referenz auf das Modell).
3. Sequenzielle Metadaten-Extraktion pro Semantic Model (Fehler werden
   übersprungen und in einer Logdatei gesammelt).
4. Bereitstellung der Formulardaten pro Dokument für die manuelle Anpassung.
5. Erzeugung aller Markdown-Dateien inkl. Übersicht und Fehler-Log unterhalb
   eines vom Benutzer gewählten Zielordners.
"""
from __future__ import annotations

import logging
import os
from dataclasses import dataclass, field
from datetime import date
from typing import Any, Callable

from core import workspace_doc_generator as wsdoc
from core.access_generator import build_report_access_table_from_users
from core.connectors.fabric_connector import FabricConnector
from core.form_generator import FormGenerator
from core.metadata_mapper import MetadataMapper
from core.models import (
    CancelledError,
    ConnectionConfig,
    ParseResult,
    ReportMetadata,
    WorkspaceItemInfo,
    WorkspaceMetadata,
)
from core.report_document_builder import render_report_markdown
from core.template_renderer import TemplateRenderer

logger = logging.getLogger(__name__)

ProgressCallback = Callable[[float, str], None]


def _safe_filename(name: str) -> str:
    """Bereinigt einen Namen für die Verwendung als Dateiname."""
    cleaned = "".join(c if c.isalnum() or c in " _-" else "_" for c in (name or ""))
    cleaned = cleaned.strip() or "Unbenannt"
    return cleaned


@dataclass
class ModelUnit:
    """Ein zu erzeugendes Semantic-Model-Dokument."""
    dataset_id: str
    name: str
    rel_path: str  # z.B. "Models/Umsatz.md"
    inline_report_id: str = ""  # gesetzt, wenn genau 1 Bericht inline dokumentiert wird
    metadata: ReportMetadata | None = None
    mappings: list | None = None
    manual_values: dict[str, str] = field(default_factory=dict)
    status: str = "pending"  # pending | extracted | error | done
    error: str = ""


@dataclass
class ReportUnit:
    """Ein eigenständiges Berichts-Dokument (mehrere Berichte pro Modell)."""
    report: WorkspaceItemInfo
    rel_path: str  # z.B. "Reports/Vertrieb.md"
    model_name: str = ""
    model_rel_path: str = ""  # Referenz zum Modell-Dokument (relativ)
    visuals: list = field(default_factory=list)
    report_users: list = field(default_factory=list)
    status: str = "pending"
    error: str = ""


class WorkspaceExporter:
    """Stateful Orchestrator für den Workspace-Bulk-Export."""

    def __init__(
        self,
        report_parse_result: ParseResult,
        template_dir: str,
        connector: FabricConnector | None = None,
        language: str = "de",
    ) -> None:
        self._report_parse = report_parse_result
        self._template_dir = template_dir
        self._connector = connector or FabricConnector()
        self._language = language
        self._mapper = MetadataMapper()
        self._form_generator = FormGenerator(locale=language)
        self._renderer = TemplateRenderer()

        self._ws: WorkspaceMetadata | None = None
        self._target_dir: str = ""
        self._only_workspace: bool = False
        self._connection_type: str = "fabric"

        self._model_units: list[ModelUnit] = []
        self._report_units: list[ReportUnit] = []
        self._model_file_map: dict[str, str] = {}  # dataset_id -> rel path
        self._report_file_map: dict[str, str] = {}  # report_id -> rel path
        self._report_count_map: dict[str, int] = {}  # dataset_id -> #reports
        self._errors: list[str] = []

    # ------------------------------------------------------------------
    # Schritt 1: Verbinden + Enumeration + Planung
    # ------------------------------------------------------------------

    def start(
        self,
        config: ConnectionConfig,
        target_dir: str,
        only_workspace: bool = False,
    ) -> dict[str, Any]:
        """Verbindet, enumeriert den Workspace und plant die Dateistruktur."""
        if not target_dir:
            raise ValueError("Kein Zielordner angegeben.")
        self._target_dir = target_dir
        self._only_workspace = only_workspace
        self._connection_type = config.connection_type or "fabric"

        self._connector.connect_workspace(config)
        self._ws = self._connector.list_workspace_contents()
        self._plan()

        return self._summary()

    def _summary(self) -> dict[str, Any]:
        assert self._ws is not None
        return {
            "workspaceName": self._ws.workspace_name,
            "workspaceId": self._ws.workspace_id,
            "modelCount": len(self._ws.datasets),
            "reportCount": len(self._ws.reports),
            "otherCount": len(self._ws.other_items),
            "userCount": len(self._ws.users),
            "onlyWorkspace": self._only_workspace,
            "documentCount": len(self._model_units),
            "items": [
                {
                    "name": d.name,
                    "type": "SemanticModel",
                    "reports": self._report_count_map.get(d.id, 0),
                }
                for d in self._ws.datasets
            ]
            + [
                {"name": r.name, "type": "Report", "dataset": r.dataset_id}
                for r in self._ws.reports
            ]
            + [{"name": o.name, "type": o.item_type} for o in self._ws.other_items],
        }

    def _plan(self) -> None:
        """Erstellt Modell-/Bericht-Einheiten und die Dateipfad-Zuordnungen."""
        assert self._ws is not None
        self._model_units = []
        self._report_units = []
        self._model_file_map = {}
        self._report_file_map = {}
        self._report_count_map = {}

        dataset_ids = {d.id for d in self._ws.datasets}
        dataset_name_map = {d.id: d.name for d in self._ws.datasets}

        # Berichte nach Semantic Model gruppieren
        reports_by_dataset: dict[str, list[WorkspaceItemInfo]] = {}
        external_reports: list[WorkspaceItemInfo] = []
        for rep in self._ws.reports:
            if rep.dataset_id and rep.dataset_id in dataset_ids:
                reports_by_dataset.setdefault(rep.dataset_id, []).append(rep)
            else:
                external_reports.append(rep)

        used_model_names: set[str] = set()
        used_report_names: set[str] = set()

        if not self._only_workspace:
            for d in self._ws.datasets:
                reps = reports_by_dataset.get(d.id, [])
                self._report_count_map[d.id] = len(reps)
                model_rel = self._unique_rel("Models", d.name, used_model_names)
                self._model_file_map[d.id] = model_rel

                unit = ModelUnit(dataset_id=d.id, name=d.name, rel_path=model_rel)

                if len(reps) == 1:
                    # Bericht inline im Modell-Dokument dokumentieren
                    unit.inline_report_id = reps[0].id
                    self._report_file_map[reps[0].id] = model_rel
                elif len(reps) > 1:
                    # Je Bericht eine eigene Datei mit Referenz aufs Modell
                    for rep in reps:
                        rep_rel = self._unique_rel(
                            "Reports", rep.name, used_report_names
                        )
                        self._report_file_map[rep.id] = rep_rel
                        self._report_units.append(
                            ReportUnit(
                                report=rep,
                                rel_path=rep_rel,
                                model_name=d.name,
                                model_rel_path=os.path.relpath(
                                    model_rel, os.path.dirname(rep_rel)
                                ).replace("\\", "/"),
                            )
                        )
                self._model_units.append(unit)

            # Berichte mit externem/fehlendem Semantic Model -> eigenständig
            for rep in external_reports:
                rep_rel = self._unique_rel("Reports", rep.name, used_report_names)
                self._report_file_map[rep.id] = rep_rel
                self._report_units.append(
                    ReportUnit(
                        report=rep,
                        rel_path=rep_rel,
                        model_name=dataset_name_map.get(rep.dataset_id, ""),
                        model_rel_path="",
                    )
                )

    def _unique_rel(self, subdir: str, name: str, used: set[str]) -> str:
        base = _safe_filename(name)
        candidate = base
        i = 2
        while candidate.lower() in used:
            candidate = f"{base}_{i}"
            i += 1
        used.add(candidate.lower())
        return f"{subdir}/{candidate}.md"

    # ------------------------------------------------------------------
    # Schritt 2: Metadaten-Extraktion
    # ------------------------------------------------------------------

    def extract_all(
        self,
        progress_callback: ProgressCallback | None = None,
        cancel_check: Callable[[], bool] | None = None,
    ) -> dict[str, Any]:
        """Extrahiert Metadaten für alle Modelle und eigenständige Berichte."""
        if self._only_workspace:
            return {"extracted": 0, "errors": 0, "documents": []}

        def _progress(p: float, msg: str) -> None:
            if progress_callback:
                progress_callback(p, msg)

        def _check() -> None:
            if cancel_check and cancel_check():
                raise CancelledError("Operation wurde abgebrochen")

        total = len(self._model_units) + len(self._report_units)
        done = 0
        extracted = 0

        for unit in self._model_units:
            _check()
            done += 1
            _progress(
                (done / total * 100) if total else 100.0,
                f"Extrahiere Semantic Model '{unit.name}' …",
            )
            try:
                self._connector.set_dataset(unit.dataset_id, unit.inline_report_id)
                metadata = self._connector.extract_metadata()
                unit.metadata = metadata
                unit.mappings = self._mapper.map(
                    self._report_parse.placeholders, metadata
                )
                unit.status = "extracted"
                extracted += 1
            except Exception as exc:  # noqa: BLE001 – Fehler überspringen + loggen
                unit.status = "error"
                unit.error = str(exc)
                msg = f"Semantic Model '{unit.name}' ({unit.dataset_id}): {exc}"
                self._errors.append(msg)
                logger.warning("Extraktion fehlgeschlagen – %s", msg)

        for runit in self._report_units:
            _check()
            done += 1
            _progress(
                (done / total * 100) if total else 100.0,
                f"Extrahiere Bericht '{runit.report.name}' …",
            )
            try:
                runit.visuals = self._connector.extract_report_visuals(
                    runit.report.id
                )
                runit.report_users = self._connector.extract_report_users(
                    runit.report.id
                )
                runit.status = "extracted"
            except Exception as exc:  # noqa: BLE001
                runit.status = "error"
                runit.error = str(exc)
                msg = f"Bericht '{runit.report.name}' ({runit.report.id}): {exc}"
                self._errors.append(msg)
                logger.warning("Visual-Extraktion fehlgeschlagen – %s", msg)

        return {
            "extracted": extracted,
            "errors": len(self._errors),
            "documents": self.get_document_list(),
        }

    # ------------------------------------------------------------------
    # Schritt 3: Sequenzielle Formulare (nur Modell-Dokumente)
    # ------------------------------------------------------------------

    def get_document_list(self) -> list[dict[str, Any]]:
        """Liste der Dokumente, die manuell angepasst werden können."""
        docs: list[dict[str, Any]] = []
        for i, unit in enumerate(self._model_units):
            if unit.status not in ("extracted", "done"):
                continue
            docs.append(
                {
                    "index": i,
                    "name": unit.name,
                    "relPath": unit.rel_path,
                    "hasInlineReport": bool(unit.inline_report_id),
                }
            )
        return docs

    def get_document_form(self, index: int) -> dict[str, Any]:
        """Formulardaten für ein Modell-Dokument (analog AppController.get_form_data)."""
        unit = self._model_units[index]
        if not unit.mappings:
            return {"sections": [], "summary": None, "name": unit.name}

        form_sections = self._form_generator.generate_form_data(unit.mappings)
        sections = []
        for section in form_sections:
            fields = []
            for f in section.fields:
                fields.append(
                    {
                        "name": f.placeholder_name,
                        "label": f.label,
                        "value": unit.manual_values.get(
                            f.placeholder_name, f.current_value
                        ),
                        "source": "auto" if f.is_auto_filled else "manual",
                        "tooltip": f.context or None,
                        "options": None,
                        "required": f.is_required,
                        "tableGroup": None,
                    }
                )
            sections.append({"title": section.section_name, "fields": fields})

        auto_count = sum(1 for m in unit.mappings if m.source == "auto")
        manual_count = sum(1 for m in unit.mappings if m.source == "manual")
        partial_count = sum(1 for m in unit.mappings if m.source == "partial")
        summary = {
            "autoCount": auto_count,
            "manualCount": manual_count,
            "partialCount": partial_count,
        }
        return {"sections": sections, "summary": summary, "name": unit.name}

    def save_document_form(self, index: int, values: dict[str, str]) -> None:
        """Speichert die manuell angepassten Werte für ein Dokument (Übernehmen)."""
        unit = self._model_units[index]
        unit.manual_values = dict(values or {})

    # ------------------------------------------------------------------
    # Schritt 4: Dateien erzeugen
    # ------------------------------------------------------------------

    def finalize(self) -> dict[str, Any]:
        """Rendert und schreibt alle Dateien inkl. Übersicht und Fehler-Log."""
        assert self._ws is not None
        ws_dir = os.path.join(
            self._target_dir, _safe_filename(self._ws.workspace_name)
        )
        os.makedirs(ws_dir, exist_ok=True)

        written: list[str] = []

        if not self._only_workspace:
            self._write_model_documents(ws_dir, written)
            self._write_report_documents(ws_dir, written)

        # Übersicht immer erzeugen
        overview_path = self._write_overview(ws_dir)
        written.append(overview_path)

        # Fehler-Log
        log_path = ""
        if self._errors:
            log_path = self._write_error_log(ws_dir)

        return {
            "outputDir": ws_dir,
            "overviewPath": overview_path,
            "fileCount": len(written),
            "errorCount": len(self._errors),
            "errorLogPath": log_path,
            "errors": list(self._errors),
        }

    def _write_model_documents(self, ws_dir: str, written: list[str]) -> None:
        for unit in self._model_units:
            if unit.status not in ("extracted", "done") or unit.metadata is None:
                continue
            try:
                content = render_report_markdown(
                    self._report_parse,
                    unit.metadata,
                    unit.mappings,
                    unit.manual_values,
                    self._connection_type,
                    language=self._language,
                    form_generator=self._form_generator,
                    renderer=self._renderer,
                )
                out = os.path.join(ws_dir, unit.rel_path.replace("/", os.sep))
                os.makedirs(os.path.dirname(out), exist_ok=True)
                with open(out, "w", encoding="utf-8") as f:
                    f.write(content)
                unit.status = "done"
                written.append(out)
            except Exception as exc:  # noqa: BLE001
                unit.status = "error"
                msg = f"Rendern des Modells '{unit.name}': {exc}"
                self._errors.append(msg)
                logger.warning(msg)

    def _write_report_documents(self, ws_dir: str, written: list[str]) -> None:
        template = wsdoc.load_template(
            self._template_dir,
            wsdoc.REPORT_TEMPLATE_FILENAME,
            wsdoc.DEFAULT_REPORT_TEMPLATE,
        )
        export_date = date.today().isoformat()

        class _VisualsHolder:
            def __init__(self, visuals: list) -> None:
                self.visuals = visuals

        for runit in self._report_units:
            try:
                access_md = build_report_access_table_from_users(
                    runit.report_users, "fabric", self._language
                )
                content = wsdoc.render_standalone_report(
                    template,
                    runit.report,
                    runit.model_name or "(externes Semantic Model)",
                    runit.model_rel_path,
                    _VisualsHolder(runit.visuals),
                    export_date,
                    report_access_md=access_md,
                )
                out = os.path.join(ws_dir, runit.rel_path.replace("/", os.sep))
                os.makedirs(os.path.dirname(out), exist_ok=True)
                with open(out, "w", encoding="utf-8") as f:
                    f.write(content)
                runit.status = "done"
                written.append(out)
            except Exception as exc:  # noqa: BLE001
                runit.status = "error"
                msg = f"Rendern des Berichts '{runit.report.name}': {exc}"
                self._errors.append(msg)
                logger.warning(msg)

    def _write_overview(self, ws_dir: str) -> str:
        assert self._ws is not None
        template = wsdoc.load_template(
            self._template_dir,
            wsdoc.WORKSPACE_TEMPLATE_FILENAME,
            wsdoc.DEFAULT_WORKSPACE_TEMPLATE,
        )
        content = wsdoc.render_overview(
            template,
            self._ws,
            self._model_file_map,
            self._report_file_map,
            self._report_count_map,
        )
        out = os.path.join(ws_dir, "Workspace-Uebersicht.md")
        with open(out, "w", encoding="utf-8") as f:
            f.write(content)
        return out

    def _write_error_log(self, ws_dir: str) -> str:
        out = os.path.join(ws_dir, "fehler-log.txt")
        with open(out, "w", encoding="utf-8") as f:
            f.write("Workspace-Export – Fehlerprotokoll\n")
            f.write("=" * 40 + "\n\n")
            for err in self._errors:
                f.write(f"- {err}\n")
        return out
