#!/usr/bin/env python3
"""run_loop.py — Harte Eval-Checks für den LinkedIn Auto-Research-Loop.

Prüft einen Post-Text gegen die harten Assertions (H1-H7) aus evals.json.
Standard-Library only, keine externen Abhängigkeiten.

Bundle-Portierung: --evals-Argument statt hardcodierter Vault-Navigation.
Default-Pfad für --evals kommt aus paths.auto_research_root + 'evals.json',
falls das Argument fehlt und die Datei an der erwarteten Stelle liegt.

Aufruf:
    python run_loop.py --mode evaluate --evals <pfad>/evals.json --text "Post-Text hier"
    python run_loop.py --mode evaluate --evals <pfad>/evals.json --file pfad/zur/datei.md
    python run_loop.py --mode evaluate --evals <pfad>/evals.json --file datei.md --json

Output: JSON mit Ergebnis pro Assertion und Gesamtscore.

Hinweis: --text und --file sind gegenseitig ausschließend (mutually exclusive).
"""

import argparse
import json
import os
import re
import sys


def split_sentences(text: str) -> list[str]:
    """Teilt Text in Sätze. Einfache Heuristik: Split bei Satzzeichen + Leerzeichen + Großbuchstabe."""
    raw = re.split(r'(?<=[.!?])\s+(?=[A-ZÄÖÜ])', text.strip())
    sentences = [s.strip() for s in raw if s.strip()]
    return sentences


def count_words(text: str) -> int:
    return len(text.split())


def count_chars(text: str) -> int:
    return len(text)


def get_first_sentence(text: str) -> str:
    sentences = split_sentences(text)
    return sentences[0] if sentences else text


def count_dashes(text: str) -> int:
    """Zählt Gedankenstriche (—, –). Bindestriche in zusammengesetzten Wörtern ignorieren."""
    em_dashes = text.count('—')
    en_dashes = text.count('–')
    # Freistehende Bindestriche (mit Leerzeichen davor und danach) als Gedankenstrich werten
    standalone_hyphens = len(re.findall(r' - ', text))
    return em_dashes + en_dashes + standalone_hyphens


def check_forbidden_words(text: str, words: list[str]) -> list[str]:
    """Gibt gefundene verbotene Wörter zurück."""
    text_lower = text.lower()
    found = []
    for word in words:
        if word.lower() in text_lower:
            found.append(word)
    return found


def check_forbidden_phrases(text: str, phrases: list[str]) -> list[str]:
    """Gibt gefundene verbotene Phrasen zurück."""
    text_lower = text.lower()
    found = []
    for phrase in phrases:
        if phrase.lower() in text_lower:
            found.append(phrase)
    return found


def has_short_sentence(text: str, max_words: int) -> bool:
    sentences = split_sentences(text)
    for s in sentences:
        if count_words(s) <= max_words:
            return True
    return False


def avg_sentence_length(text: str) -> float:
    sentences = split_sentences(text)
    if not sentences:
        return 0.0
    total_words = sum(count_words(s) for s in sentences)
    return total_words / len(sentences)


def evaluate_hard(text: str, evals: dict) -> dict:
    """Prüft alle harten Assertions und gibt Ergebnis zurück."""
    results = {}
    hard = evals['assertions']['hard']

    for assertion in hard:
        aid = assertion['id']
        check = assertion['check']
        passed = False
        detail = ''

        if check == 'char_count_range':
            chars = count_chars(text)
            passed = assertion['min'] <= chars <= assertion['max']
            detail = f'{chars} Zeichen'

        elif check == 'first_sentence_length':
            first = get_first_sentence(text)
            chars = count_chars(first)
            words = count_words(first)
            passed = chars <= assertion['max_chars'] and words <= assertion['max_words']
            detail = f'{chars} Zeichen, {words} Wörter'

        elif check == 'dash_count':
            dashes = count_dashes(text)
            passed = dashes <= assertion['max']
            detail = f'{dashes} Gedankenstriche'

        elif check == 'forbidden_words':
            found = check_forbidden_words(text, assertion['words'])
            passed = len(found) == 0
            detail = f'Gefunden: {", ".join(found)}' if found else 'Keine Weichmacher'

        elif check == 'short_sentence_exists':
            passed = has_short_sentence(text, assertion['max_words'])
            detail = 'Kurzsatz vorhanden' if passed else 'Kein Satz ≤5 Wörter'

        elif check == 'avg_sentence_length':
            avg = avg_sentence_length(text)
            passed = avg <= assertion['max_avg']
            detail = f'Ø {avg:.1f} Wörter/Satz'

        elif check == 'forbidden_phrases':
            found = check_forbidden_phrases(text, assertion['phrases'])
            passed = len(found) == 0
            detail = f'Gefunden: {", ".join(found)}' if found else 'Keine KI-Floskeln'

        results[aid] = {
            'text': assertion['text'],
            'passed': passed,
            'detail': detail
        }

    return results


def extract_entwurf(md_content: str) -> str:
    """Extrahiert den Text aus dem ## Entwurf-Abschnitt einer Markdown-Datei."""
    pattern = r'## Entwurf\s*\n(.*?)(?=\n## |\Z)'
    match = re.search(pattern, md_content, re.DOTALL)
    if match:
        return match.group(1).strip()
    return md_content.strip()


def load_evals(evals_path: str | None) -> dict:
    """Lädt evals.json aus dem angegebenen Pfad.

    Der --evals-Parameter muss explizit gesetzt sein. Ohne ihn schlägt das
    Skript deterministisch fehl — kein stochastischer Fallback.
    """
    if evals_path is None:
        print(
            'Fehler: --evals ist erforderlich. Übergebe den Pfad zur evals.json '
            '(z.B. --evals <auto_research_root>/evals.json).',
            file=sys.stderr,
        )
        sys.exit(1)
    if not os.path.isfile(evals_path):
        print(f'Fehler: evals.json nicht gefunden: {evals_path}', file=sys.stderr)
        sys.exit(1)
    with open(evals_path, 'r', encoding='utf-8') as f:
        try:
            data = json.load(f)
        except json.JSONDecodeError as exc:
            print(f'Fehler: evals.json ist kein gültiges JSON: {exc}', file=sys.stderr)
            sys.exit(1)
    # Schema-Validierung (F-MED-1 v0.14.1): assertions.hard muss als Liste vorhanden sein.
    if not isinstance(data, dict) or 'assertions' not in data or not isinstance(data.get('assertions'), dict) or 'hard' not in data['assertions'] or not isinstance(data['assertions']['hard'], list):
        print(
            f'Fehler: evals.json hat ungültiges Schema. Erwartet: {{"assertions": {{"hard": [...]}}}}. '
            f'Pfad: {evals_path}',
            file=sys.stderr,
        )
        sys.exit(1)
    return data


def main():
    parser = argparse.ArgumentParser(description='LinkedIn Post Hard Eval Checker')
    parser.add_argument('--mode', choices=['evaluate'], required=True)
    parser.add_argument('--evals', help='Pfad zur evals.json (Pflichtargument)')

    # --text und --file sind gegenseitig ausschließend
    input_group = parser.add_mutually_exclusive_group()
    input_group.add_argument('--text', help='Post-Text direkt als Argument')
    input_group.add_argument('--file', help='Pfad zu einer Markdown-Datei mit ## Entwurf')

    parser.add_argument('--json', action='store_true', help='Output als JSON')
    args = parser.parse_args()

    # Text laden
    if args.text:
        text = args.text
    elif args.file:
        # File-Existence-Check (F-MED-2 v0.14.1): sauberer Fehler statt FileNotFoundError-Traceback.
        if not os.path.isfile(args.file):
            print(f'Fehler: Post-Datei nicht gefunden: {args.file}', file=sys.stderr)
            sys.exit(1)
        with open(args.file, 'r', encoding='utf-8') as f:
            content = f.read()
        text = extract_entwurf(content)
    else:
        print('Fehler: Kein Text angegeben. Nutze --text oder --file.', file=sys.stderr)
        sys.exit(1)

    if not text:
        print('Fehler: Leerer Text nach Extraktion.', file=sys.stderr)
        sys.exit(1)

    evals = load_evals(args.evals)
    results = evaluate_hard(text, evals)

    passed_count = sum(1 for r in results.values() if r['passed'])
    total = len(results)

    if args.json:
        output = {
            'hard_results': results,
            'hard_score': f'{passed_count}/{total}',
            'hard_passed': passed_count,
            'hard_total': total
        }
        print(json.dumps(output, ensure_ascii=False, indent=2))
    else:
        print(f'Hard-Score: {passed_count}/{total}')
        print()
        for aid, result in results.items():
            status = '✓' if result['passed'] else '✗'
            print(f'  {aid} {status}  {result["detail"]}')
            if not result['passed']:
                print(f'       → {result["text"]}')


if __name__ == '__main__':
    main()
