"""Scan-Dienst: Vorflug-Checks, Re-Scan-Planung und Hintergrund-Läufe.

Eine Wahrheit für CLI und Server: cli.py und routes.py delegieren beide hierher.
Doppelstart-Guard nach Creator-Muster: prozess-lokales Dict + idempotente Antwort +
Re-Entry-Check in der Coroutine (gilt nur im eigenen Prozess — bei laufendem Server
Scans über die UI starten, nicht parallel per CLI).
"""
import asyncio
import logging
import os
import time
from pathlib import Path

import agents
import artefakte
import chunker
import config
import repo
import scan_board

log = logging.getLogger("planer.dienst")


class VorflugFehler(Exception):
    """Lauf darf nicht starten (kein Repo, dirty tree, Rolle/Provider unbrauchbar)."""


def vorflug(wurzel: Path) -> str:
    """Checks vor jedem Lauf → HEAD-Hash. Wirft VorflugFehler (Erkenntnis 13:
    lieber vorher stoppen als leer verbrennen)."""
    if not wurzel.is_dir():
        raise VorflugFehler(f"{wurzel} ist kein Verzeichnis")
    if not repo.ist_repo(wurzel):
        raise VorflugFehler(f"{wurzel} ist kein git-Repo — Anker brauchen Commit-Hashes (git init && commit)")
    if not repo.working_tree_sauber(wurzel):
        raise VorflugFehler(f"{wurzel} hat uncommittete Änderungen — erst committen, dann scannen")
    if not config.fake_agents_aktiv():
        for rolle in config.ROLLEN:
            try:
                provider, _ = config.resolve_role(rolle)
            except RuntimeError as e:
                raise VorflugFehler(str(e)) from e
            if not agents.provider_available(provider):
                raise VorflugFehler(f"Provider '{provider}' (Rolle {rolle}) nicht nutzbar — CLI/Key prüfen")
    return repo.head_hash(wurzel)


def einheiten_bestand(wurzel: Path) -> dict[str, tuple[str, str]]:
    """{quelldatei: (stand, text)} aus den vorhandenen .planer/einheiten/-Dateien."""
    out: dict[str, tuple[str, str]] = {}
    verzeichnis = wurzel / ".planer" / "einheiten"
    for p in verzeichnis.glob("*.md") if verzeichnis.is_dir() else []:
        text = p.read_text(encoding="utf-8")
        kopf = text.splitlines()[0] if text else ""
        datei = kopf.removeprefix("# Einheiten:").strip()
        stand = artefakte.lese_stand(text)
        if datei and stand:
            out[datei] = (stand, text)
    return out


def rescan_plan(wurzel: Path) -> tuple[set[str], dict[str, str], list[str]]:
    """→ (zu_scannen, unveraendert {datei: text}, geloeschte_quelldateien).
    Wirft VorflugFehler, wenn noch kein Scan existiert."""
    bestand = einheiten_bestand(wurzel)
    if not bestand:
        raise VorflugFehler("keine .planer/einheiten/ gefunden — erst `scan` ausführen")
    scanbar = {str(p.relative_to(wurzel)) for p in chunker.dateien(wurzel)}
    diff_cache: dict[str, set[str]] = {}

    def geaendert_seit(stand: str) -> set[str]:
        if stand not in diff_cache:
            diff_cache[stand] = repo.geaenderte_dateien(wurzel, stand)[0]
        return diff_cache[stand]

    zu_scannen: set[str] = scanbar - set(bestand)
    unveraendert: dict[str, str] = {}
    for datei, (stand_alt, text) in bestand.items():
        if datei not in scanbar:
            continue
        if stand_alt == "UNBEKANNT" or datei in geaendert_seit(stand_alt):
            zu_scannen.add(datei)
        else:
            unveraendert[datei] = text
    geloescht = [datei for datei in bestand if datei not in scanbar]
    return zu_scannen, unveraendert, geloescht


async def rescan_lauf(wurzel: Path, projekt: str, stand: str, set_p=None) -> dict:
    """Kompletter Re-Scan: Plan, Aufräumen gelöschter Artefakte, Lauf. Keine Änderungen →
    {status: 'unveraendert'}."""
    zu_scannen, unveraendert, geloescht = rescan_plan(wurzel)
    for datei in geloescht:
        (wurzel / ".planer" / artefakte.datei_pfad(datei)).unlink(missing_ok=True)
    if not zu_scannen and not geloescht:
        return {"status": "unveraendert", "geschrieben": [], "dead": []}
    if not zu_scannen and unveraendert:
        # Nur Löschungen: Sichten müssen neu — kleinste verbliebene Datei mit-rescannen
        zu_scannen = {min(unveraendert, key=lambda d: len(unveraendert[d]))}
        unveraendert.pop(next(iter(zu_scannen)))
    log.info("rescan %s: %d neu, %d unverändert, %d gelöscht",
             projekt, len(zu_scannen), len(unveraendert), len(geloescht))
    return await scan_board.run_scan(wurzel, projekt, stand, set_p=set_p,
                                     nur_dateien=zu_scannen, bestand=unveraendert)


# --- Hintergrund-Läufe (Server) --------------------------------------------------------

_laeufe: dict[str, dict] = {}  # projekt → {task, art, gestartet, ergebnis}


def starte(name: str, wurzel: Path, art: str, fortsetzen: bool = False) -> str:
    """Startet scan|rescan als Hintergrund-Task. → 'gestartet' | 'laeuft'.
    Wirft VorflugFehler synchron (Route antwortet 409 mit Meldung)."""
    eintrag = _laeufe.get(name)
    if eintrag and not eintrag["task"].done():
        return "laeuft"
    stand = vorflug(wurzel)
    if art == "rescan":
        rescan_plan(wurzel)  # wirft synchron, wenn es keinen Erst-Scan gibt

    async def lauf():
        if (e := _laeufe.get(name)) and e.get("laeuft_intern"):  # Re-Entry-Schutz
            return
        _laeufe[name]["laeuft_intern"] = True
        try:
            if art == "rescan":
                ergebnis = await rescan_lauf(wurzel, name, stand)
            else:
                ergebnis = await scan_board.run_scan(wurzel, name, stand, fortsetzen=fortsetzen)
            _laeufe[name]["ergebnis"] = ergebnis
        except Exception as e:  # Status muss den Fehler zeigen, nie stiller Tod
            log.exception("lauf %s (%s) abgebrochen", name, art)
            _laeufe[name]["ergebnis"] = {"status": "abbruch", "infra_error": str(e),
                                         "geschrieben": [], "dead": []}

    _laeufe[name] = {"task": asyncio.create_task(lauf()), "art": art,
                     "gestartet": time.time(), "ergebnis": None}
    return "gestartet"


def status(name: str) -> dict | None:
    eintrag = _laeufe.get(name)
    if eintrag is None:
        return None
    return {"laeuft": not eintrag["task"].done(), "art": eintrag["art"],
            "gestartet": eintrag["gestartet"], "ergebnis": eintrag["ergebnis"]}
