#!/usr/bin/env python3
"""Heuristischer Prompt-Schulden-Detektor ohne externe Abhängigkeiten.

Der Detektor markiert Kandidaten. Er kann nicht entscheiden, ob eine Sicherheits-
oder Produktregel fachlich notwendig ist. Prüfen Sie jeden Treffer im Kontext und
vergleichen Sie Änderungen an realen Aufgaben.

Verwendung:
    python prompt_schulden_detektor.py "Ihr Prompt"
    python prompt_schulden_detektor.py pfad/zum/prompt.md
    cat prompt.txt | python prompt_schulden_detektor.py --stdin
    python prompt_schulden_detektor.py --stdin --json
"""

from __future__ import annotations

import argparse
import json
import re
import sys
from collections import Counter, defaultdict
from pathlib import Path
from typing import Iterable


class PromptSchuldenDetektor:
    """Findet auffällige Regeln mit bewusst konservativen Heuristiken."""

    def __init__(
        self,
        enable_outdated: bool = True,
        enable_forbidden: bool = True,
        enable_repetitions: bool = True,
    ) -> None:
        self.enable_outdated = enable_outdated
        self.enable_forbidden = enable_forbidden
        self.enable_repetitions = enable_repetitions

        self.severity = {
            "outdated": "info",
            "forbidden": "warning",
            "repetition": "warning",
            "contradiction": "critical",
        }

        self.outdated_patterns = [
            (
                re.compile(
                    r"\b(?:du|Sie)\s+(?:bist|sind)\s+(?:ein(?:e)?\s+)?"
                    r"(?:KI-Assistent|Sprachmodell|künstliche\s+Intelligenz|Assistent|KI)\b",
                    re.IGNORECASE,
                ),
                "Die Selbstidentifikation beschreibt das Modell statt der Aufgabe.",
                "Entfernen, sofern die Rolle nicht für die konkrete Ausgabe nötig ist.",
            ),
            (
                re.compile(
                    r"\b(?:denke\s+daran|erinnere\s+dich|vergiss\s+nicht)\s*,?\s*"
                    r"(?:dass\s+)?(?:du|Sie)\s+(?:ein(?:e)?\s+)?"
                    r"(?:Sprachmodell|KI|Assistent|kein\s+Mensch)\s+(?:bist|sind)?\b",
                    re.IGNORECASE,
                ),
                "Die Erinnerung an die KI-Natur steuert kein fachliches Ergebnis.",
                "Entfernen oder durch ein konkretes Ausgabeziel ersetzen.",
            ),
            (
                re.compile(
                    r"\b(?:erwähne|sage)\s+nicht\s*,?\s*(?:dass\s+)?du\s+"
                    r"(?:eine?\s+)?(?:KI|Modell|Assistent)\s+bist\b|"
                    r"\b(?:erwähnen|sagen)\s+Sie\s+nicht\s*,?\s*(?:dass\s+)?Sie\s+"
                    r"(?:eine?\s+)?(?:KI|Modell|Assistent)\s+sind\b",
                    re.IGNORECASE,
                ),
                "Das Verbot betrifft die Selbstdarstellung des Modells, nicht die Aufgabe.",
                "Nur behalten, wenn die Formulierung eine verbindliche Produktanforderung ist.",
            ),
            (
                re.compile(
                    r"\b(?:du|Sie)\s+(?:hast|haben|besitzt|besitzen)\s+"
                    r"(?:kein|keine|keinen)\s+(?:Bewusstsein|Emotionen|Gefühle)\b",
                    re.IGNORECASE,
                ),
                "Die Aussage über Bewusstsein oder Gefühle hilft selten bei der Aufgabe.",
                "Entfernen, sofern sie nicht Gegenstand der gewünschten Antwort ist.",
            ),
        ]

        object_text = r"(?P<object>[^.!?\n]{2,120})"
        self.forbidden_patterns = [
            re.compile(
                r"\b(?:verwende|benutze|nutze|verwenden\s+Sie|benutzen\s+Sie|nutzen\s+Sie)\s+"
                r"(?:bitte\s+)?(?:nicht|kein(?:e|en|er|es)?)\s+" + object_text,
                re.IGNORECASE,
            ),
            re.compile(
                r"\b(?:sage|erwähne|antworte|sagen\s+Sie|erwähnen\s+Sie|antworten\s+Sie)\s+"
                r"(?:bitte\s+)?nicht\s+" + object_text,
                re.IGNORECASE,
            ),
            re.compile(
                r"\b(?:vermeide|vermeiden\s+Sie)\s+" + object_text,
                re.IGNORECASE,
            ),
            re.compile(
                r"\b(?:es\s+ist\s+verboten|verboten\s+ist)\s+" + object_text,
                re.IGNORECASE,
            ),
            re.compile(
                r"\b(?:niemals|auf\s+keinen\s+Fall)\s+" + object_text,
                re.IGNORECASE,
            ),
        ]

        self.contradiction_pairs = [
            (
                re.compile(r"\b(?:sei|seien\s+Sie|antworte|antworten\s+Sie)\s+kurz\b", re.IGNORECASE),
                re.compile(
                    r"\b(?:gib|geben\s+Sie)\s+(?:eine\s+)?ausführliche\s+Antwort\b|"
                    r"\bantworte\s+ausführlich\b",
                    re.IGNORECASE,
                ),
                "Kürze und Ausführlichkeit werden gleichzeitig verlangt.",
                "Bedingungen ergänzen: wann kurz, wann ausführlich.",
            ),
            (
                re.compile(
                    r"\b(?:verwende|benutze|verwenden\s+Sie|benutzen\s+Sie)\s+"
                    r"(?:keinen|nicht)\s+Slang\b|\b(?:vermeide|vermeiden\s+Sie)\s+Slang\b",
                    re.IGNORECASE,
                ),
                re.compile(
                    r"\b(?:verwende|benutze|verwenden\s+Sie|benutzen\s+Sie)\s+Slang\b",
                    re.IGNORECASE,
                ),
                "Slang wird zugleich verboten und verlangt.",
                "Eine Regel entfernen oder die Zielgruppen als Bedingungen nennen.",
            ),
            (
                re.compile(
                    r"\b(?:verwende|verwenden\s+Sie)\s+(?:keine|nicht)\s+Listen\b|"
                    r"\b(?:vermeide|vermeiden\s+Sie)\s+Listen\b",
                    re.IGNORECASE,
                ),
                re.compile(
                    r"\b(?:verwende|verwenden\s+Sie)\s+(?:immer\s+)?Listen\b",
                    re.IGNORECASE,
                ),
                "Listen werden zugleich verboten und verlangt.",
                "Den passenden Ausgabetyp je Aufgabe als Bedingung formulieren.",
            ),
        ]

    @staticmethod
    def _normalisiere_objekt(text: str) -> str:
        text = text.casefold().strip(" \t.,!?;:()[]{}\"'„“‚‘")
        text = re.sub(r"\b(?:der|die|das|den|dem|des|ein|eine|einen|einem|einer)\b", " ", text)
        text = re.sub(r"\s+", " ", text)
        return text.strip()

    @staticmethod
    def _overlaps(a: dict, b: dict) -> bool:
        start = max(a["start"], b["start"])
        end = min(a["end"], b["end"])
        if end <= start:
            return False
        shorter = min(a["end"] - a["start"], b["end"] - b["start"])
        return (end - start) / max(shorter, 1) >= 0.8

    def _finde_veraltete_muster(self, text: str) -> list[dict]:
        findings = []
        for pattern, explanation, recommendation in self.outdated_patterns:
            for match in pattern.finditer(text):
                findings.append(
                    {
                        "category": "outdated",
                        "severity": self.severity["outdated"],
                        "snippet": match.group(0).strip(),
                        "explanation": explanation,
                        "recommendation": recommendation,
                        "start": match.start(),
                        "end": match.end(),
                    }
                )
        return findings

    def _finde_verbote(self, text: str) -> list[dict]:
        findings = []
        for pattern in self.forbidden_patterns:
            for match in pattern.finditer(text):
                findings.append(
                    {
                        "category": "forbidden",
                        "severity": self.severity["forbidden"],
                        "snippet": match.group(0).strip(),
                        "object": self._normalisiere_objekt(match.group("object")),
                        "explanation": (
                            "Die absolute Formulierung kann eine sinnvolle Ausnahme blockieren. "
                            "Die Heuristik kann ihre fachliche Notwendigkeit nicht beurteilen."
                        ),
                        "recommendation": (
                            "Prüfen, ob eine echte Sicherheitsgrenze vorliegt; andernfalls als "
                            "Bedingung formulieren und im Vergleichstest prüfen."
                        ),
                        "start": match.start(),
                        "end": match.end(),
                    }
                )
        return findings

    def _finde_wiederholungen(self, prohibitions: Iterable[dict]) -> list[dict]:
        groups: defaultdict[str, list[dict]] = defaultdict(list)
        for finding in prohibitions:
            obj = finding.get("object", "")
            if obj:
                groups[obj].append(finding)

        findings = []
        for obj, items in groups.items():
            if len(items) < 2:
                continue
            findings.append(
                {
                    "category": "repetition",
                    "severity": self.severity["repetition"],
                    "snippet": " | ".join(item["snippet"] for item in items),
                    "phrase": obj,
                    "count": len(items),
                    "explanation": f'Dasselbe Verbotsobjekt „{obj}“ erscheint {len(items)} Mal.',
                    "recommendation": "Zu einer einzigen, möglichst bedingten Regel zusammenführen.",
                    "start": min(item["start"] for item in items),
                    "end": max(item["end"] for item in items),
                }
            )
        return findings

    def _finde_widersprueche(self, text: str) -> list[dict]:
        findings = []
        for left_pattern, right_pattern, explanation, recommendation in self.contradiction_pairs:
            left = left_pattern.search(text)
            right = right_pattern.search(text)
            if not left or not right:
                continue
            findings.append(
                {
                    "category": "contradiction",
                    "severity": self.severity["contradiction"],
                    "snippet": f'„{left.group(0)}“ vs. „{right.group(0)}“',
                    "explanation": explanation,
                    "recommendation": recommendation,
                    "start": min(left.start(), right.start()),
                    "end": max(left.end(), right.end()),
                }
            )
        return findings

    def analysiere(self, prompt_text: str) -> dict:
        outdated = self._finde_veraltete_muster(prompt_text) if self.enable_outdated else []
        prohibitions = self._finde_verbote(prompt_text) if self.enable_forbidden else []

        # Eine modellbezogene Altlast wird nicht zusätzlich als allgemeines Verbot gezählt.
        prohibitions = [
            finding
            for finding in prohibitions
            if not any(self._overlaps(finding, old) for old in outdated)
        ]

        findings = [*outdated, *prohibitions]
        if self.enable_repetitions:
            findings.extend(self._finde_wiederholungen(prohibitions))
        findings.extend(self._finde_widersprueche(prompt_text))
        findings.sort(key=lambda item: (item["start"], item["end"], item["category"]))

        counts = Counter(finding["category"] for finding in findings)
        return {
            "total_findings": len(findings),
            "counts": dict(sorted(counts.items())),
            "findings": findings,
            "limitations": (
                "Heuristische Prüfung: Treffer sind Kandidaten, keine automatische "
                "Löschentscheidung. Sicherheits- und Produktregeln müssen fachlich geprüft werden."
            ),
        }

    def formatiere_bericht_text(self, report: dict) -> str:
        if report["total_findings"] == 0:
            return "Keine Prompt-Schulden gefunden."

        category_names = {
            "outdated": "Veraltete Muster",
            "forbidden": "Potenziell überflüssige Verbote",
            "repetition": "Wiederholte Anforderungen",
            "contradiction": "Widersprüchliche Anweisungen",
        }
        order = ("outdated", "forbidden", "repetition", "contradiction")
        grouped: defaultdict[str, list[dict]] = defaultdict(list)
        for finding in report["findings"]:
            grouped[finding["category"]].append(finding)

        lines = [f"Potenzielle Probleme gefunden: {report['total_findings']}", ""]
        for category in order:
            if not grouped[category]:
                continue
            lines.append(category_names[category] + ":")
            for finding in grouped[category]:
                lines.extend(
                    [
                        f"- Fragment: {finding['snippet']}",
                        f"  Warum: {finding['explanation']}",
                        f"  Empfehlung: {finding['recommendation']}",
                        f"  Schweregrad: {finding['severity']}",
                    ]
                )
            lines.append("")
        lines.append("Hinweis: " + report["limitations"])
        return "\n".join(lines)

    @staticmethod
    def formatiere_bericht_json(report: dict) -> str:
        return json.dumps(report, ensure_ascii=False, indent=2)


def _looks_like_path(value: str) -> bool:
    path = Path(value)
    return (
        value.startswith(("/", "./", "../", "~"))
        or path.suffix.casefold() in {".txt", ".md", ".prompt", ".json", ".yaml", ".yml"}
    )


def _read_input(value: str) -> str:
    path = Path(value).expanduser()
    if path.is_file():
        return path.read_text(encoding="utf-8")
    if _looks_like_path(value):
        raise FileNotFoundError(value)
    return value


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Prompt-Schulden heuristisch erkennen")
    source = parser.add_mutually_exclusive_group(required=True)
    source.add_argument("input", nargs="?", help="Prompt-Text oder Pfad zu einer Textdatei")
    source.add_argument("--stdin", action="store_true", help="Prompt aus stdin lesen")
    parser.add_argument("--json", action="store_true", help="Ausgabe im JSON-Format")
    parser.add_argument("--no-outdated", action="store_true", help="Veraltete Muster nicht prüfen")
    parser.add_argument("--no-forbidden", action="store_true", help="Verbote nicht prüfen")
    parser.add_argument("--no-repetitions", action="store_true", help="Wiederholungen nicht prüfen")
    return parser


def main(argv: list[str] | None = None) -> int:
    args = build_parser().parse_args(argv)
    try:
        prompt_text = sys.stdin.read() if args.stdin else _read_input(args.input)
    except (FileNotFoundError, OSError) as error:
        print(f"Datei nicht gefunden: {error}", file=sys.stderr)
        return 2

    detector = PromptSchuldenDetektor(
        enable_outdated=not args.no_outdated,
        enable_forbidden=not args.no_forbidden,
        enable_repetitions=not args.no_repetitions,
    )
    report = detector.analysiere(prompt_text)
    if args.json:
        print(detector.formatiere_bericht_json(report))
    else:
        print(detector.formatiere_bericht_text(report))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
