#!/usr/bin/env python3
"""
Aria Eval-Case-Miner (KAR-78 Aria-Track, P13-Adopt aus 15-Projects-Analyse)

Mined Supabase aria_chat_log nach Failure-Interaktionen (Kais korrigiert,
Frust-Signal, Wiederholungs-Bitte) und konvertiert sie in Eval-Case-KANDIDATEN.

Wichtig: schreibt NIE direkt in das Live-Gate /root/aria/config/eval-lint-cases.yaml.
Kandidaten landen in eval-lint-case-candidates.yaml + Brain-Report zur Review.
Promotion ins Live-Gate bleibt manueller Schritt (Aria + Kais), analog
Memory-Curator-Pattern (kein Auto-Mutate an aktiven Gates).

Usage:
  python3 aria-eval-miner.py [days]      # Default 14 Tage
  python3 aria-eval-miner.py --self-test # Detector-Tests, kein Netz

Pipeline:
  1. Chat-Log fetchen (Supabase REST, gleiche Quelle wie aria-self-audit.py)
  2. User-Korrektur-Signale erkennen, vorangehende Aria-Antwort einfangen
  3. Dedupe gegen state/eval-miner-seen.json (idempotent bei Re-Runs)
  4. Kandidaten appenden + Report nach 02-Wissen/eval-miner-reports/

NOTE(DRY): load_env/fetch sind bewusst aus aria-self-audit.py dupliziert —
Skripte mit Bindestrich-Namen sind nicht importierbar; gemeinsames Modul
lohnt erst ab 3. Consumer (ENGINEERING.md Regel 1 Ausnahme).
"""
import hashlib
import json
import os
import re
import sys
import urllib.request
from datetime import datetime, timezone, timedelta

BRAIN = "/root/aria/brain"
CANDIDATES_FILE = "/root/aria/config/eval-lint-case-candidates.yaml"
SEEN_FILE = "/root/aria/state/eval-miner-seen.json"
REPORT_DIR = f"{BRAIN}/02-Wissen/eval-miner-reports"

# Korrektur-/Failure-Signale in Kais-Inbounds. Erweiterung von
# FRUSTRATION_PATTERNS in aria-self-audit.py um Korrektur-Spezifika:
# wir wollen nicht nur "Kais ist frustriert" sondern "Aria-Output davor
# war vermutlich falsch" — das ist der Eval-Case-Rohstoff.
CORRECTION_PATTERNS = [
    (r"\b(falsch|verkehrt|stimmt nicht|nicht richtig)\b", "faktisch-falsch"),
    (r"\b(nein|doch nicht|nicht so)\b", "richtungs-korrektur"),
    (r"(hast du nicht|nicht gemacht|fehlt|vergessen)", "unterlassung"),
    (r"(schon wieder|wieder mal|hatte dich .*gebeten|wie oft)", "wiederholungs-fehler"),
    (r"(hallo\?|bist du da|lebst du noch)", "stille-timeout"),
    (r"(kam nicht an|sehe nichts|wo ist|nichts erhalten)", "delivery-fehlschlag"),
    (r"\?\?+|!!+", "eskalations-zeichen"),
    (r"(müll|mist|kacke|scheiße|unbrauchbar)", "qualitaets-frust"),
]

# Inbounds, die wie Korrekturen aussehen, aber keine sind (Mining-Rauschen).
NOISE_PATTERNS = [
    r"^(ok|ja|nein danke|passt|gut|top|danke)\W*$",
    r"^\d\W*$",          # Menü-Antworten "1", "2."
    r"^[a-d]\W*$",       # Menü-Antworten "A"
]


def load_env():
    env_files = ["/root/aria/.env", "/root/aria/scripts/.env.aria",
                 "/root/.claude/channels/telegram/.env", "/root/.claude/.env"]
    for f in env_files:
        if not os.path.exists(f):
            continue
        for line in open(f):
            line = line.strip()
            if line and not line.startswith("#") and "=" in line:
                k, _, v = line.partition("=")
                os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))


def fetch_chat_supabase(days):
    url = os.environ.get("SUPABASE_URL") or os.environ.get("NEXT_PUBLIC_SUPABASE_URL")
    key = os.environ.get("SUPABASE_SERVICE_ROLE_KEY") or os.environ.get("SUPABASE_KEY")
    if not url or not key:
        print("[err] SUPABASE_URL/KEY fehlen — .env nicht geladen?", file=sys.stderr)
        return []
    cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
    req_url = (
        f"{url}/rest/v1/aria_chat_log"
        f"?select=created_at,direction,message_text"
        f"&created_at=gte.{cutoff}"
        f"&order=created_at.asc"
        f"&limit=5000"
    )
    req = urllib.request.Request(req_url, headers={"apikey": key, "Authorization": f"Bearer {key}"})
    try:
        with urllib.request.urlopen(req, timeout=15) as r:
            return json.loads(r.read())
    except Exception as e:
        print(f"[supabase err: {e}]", file=sys.stderr)
        return []


def detect_signal(text):
    """Liefert Signal-Label fuer eine User-Message oder None."""
    low = (text or "").strip().lower()
    if not low:
        return None
    for pat in NOISE_PATTERNS:
        if re.match(pat, low):
            return None
    for pat, label in CORRECTION_PATTERNS:
        if re.search(pat, low):
            return label
    return None


def mine_candidates(rows):
    """Walk chronologisch: User-Korrektur → vorangehende Aria-Antwort = Failure-Kandidat."""
    candidates = []
    last_assistant = None
    for row in rows:
        text = row.get("message_text") or ""
        if row.get("direction") != "in":
            last_assistant = row
            continue
        label = detect_signal(text)
        if not label or last_assistant is None:
            continue
        failed_output = (last_assistant.get("message_text") or "")[:400]
        candidates.append({
            "signal": label,
            "at": row.get("created_at", ""),
            "user_correction": text[:200],
            "failed_aria_output": failed_output,
            # Hash ueber Korrektur+Output: stabiler Dedupe-Key ueber Re-Runs
            "key": hashlib.sha1((label + text[:200] + failed_output).encode()).hexdigest()[:16],
        })
    return candidates


def load_seen():
    if os.path.exists(SEEN_FILE):
        try:
            return set(json.load(open(SEEN_FILE)))
        except Exception:
            return set()
    return set()


def save_seen(seen):
    os.makedirs(os.path.dirname(SEEN_FILE), exist_ok=True)
    json.dump(sorted(seen), open(SEEN_FILE, "w"), indent=0)


def yaml_quote(s):
    return '"' + s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", " ") + '"'


def append_candidates(fresh):
    os.makedirs(os.path.dirname(CANDIDATES_FILE), exist_ok=True)
    is_new = not os.path.exists(CANDIDATES_FILE)
    with open(CANDIDATES_FILE, "a") as f:
        if is_new:
            f.write("# Eval-Case-KANDIDATEN aus aria-eval-miner.py (KAR-78 Aria-Track)\n")
            f.write("# Review noetig — Promotion nach eval-lint-cases.yaml ist manuell.\n")
            f.write("candidates:\n")
        for c in fresh:
            f.write(f"\n  - key: {c['key']}\n")
            f.write(f"    signal: {c['signal']}\n")
            f.write(f"    at: {yaml_quote(c['at'])}\n")
            f.write(f"    user_correction: {yaml_quote(c['user_correction'])}\n")
            f.write(f"    failed_aria_output: {yaml_quote(c['failed_aria_output'])}\n")
            f.write("    status: pending-review\n")


def write_report(fresh, total_msgs, days):
    os.makedirs(REPORT_DIR, exist_ok=True)
    today = datetime.now().strftime("%Y-%m-%d")
    path = f"{REPORT_DIR}/{today}.md"
    by_signal = {}
    for c in fresh:
        by_signal.setdefault(c["signal"], []).append(c)
    lines = [
        "---",
        f"title: Eval-Miner-Report {today}",
        "type: audit",
        "tags: [eval, mining, kar-78]",
        f"date: {today}",
        "status: aktiv",
        "---",
        "",
        f"# Eval-Miner-Report {today}",
        "",
        f"Fenster: {days} Tage · Messages gescannt: {total_msgs} · neue Kandidaten: {len(fresh)}",
        "",
    ]
    for signal, items in sorted(by_signal.items()):
        lines.append(f"## {signal} ({len(items)})")
        for c in items[:5]:
            lines.append(f"- `{c['at']}` Kais: „{c['user_correction'][:120]}“")
        lines.append("")
    if not fresh:
        lines.append("Keine neuen Kandidaten — keine unverarbeiteten Korrektur-Signale im Fenster.")
    lines.append("")
    lines.append(f"Kandidaten-File: `{CANDIDATES_FILE}` · Promotion manuell nach Review.")
    with open(path, "w") as f:
        f.write("\n".join(lines) + "\n")
    return path


def self_test():
    fixtures = [
        ("Das ist falsch, der Pfad existiert nicht", "faktisch-falsch"),
        ("Nein, nicht so — erst der Branch", "richtungs-korrektur"),
        ("Du hast den Report vergessen", "unterlassung"),
        ("Schon wieder kein Telegram-Format??", "wiederholungs-fehler"),
        ("Hallo? Bist du da", "stille-timeout"),
        ("ok", None),
        ("1", None),
        ("A", None),
        ("Danke", None),
        ("", None),
    ]
    failures = 0
    for text, expected in fixtures:
        got = detect_signal(text)
        status = "ok" if got == expected else "FAIL"
        if got != expected:
            failures += 1
        print(f"  [{status}] {text!r:55} -> {got!r} (erwartet {expected!r})")
    # Pipeline-Test: Korrektur nach Aria-Output ergibt 1 Kandidat, Noise keinen
    rows = [
        {"direction": "out", "message_text": "Fertig, PR ist gemerged", "created_at": "t1"},
        {"direction": "in", "message_text": "Das ist falsch, nichts gemerged", "created_at": "t2"},
        {"direction": "in", "message_text": "ok", "created_at": "t3"},
    ]
    cands = mine_candidates(rows)
    ok = len(cands) == 1 and cands[0]["signal"] == "faktisch-falsch"
    print(f"  [{'ok' if ok else 'FAIL'}] mine_candidates pipeline -> {len(cands)} Kandidat(en)")
    if not ok:
        failures += 1
    print(f"self-test: {'PASS' if failures == 0 else f'{failures} FAILURES'}")
    return 0 if failures == 0 else 1


def main():
    if "--self-test" in sys.argv:
        sys.exit(self_test())
    days = int(sys.argv[1]) if len(sys.argv) > 1 and sys.argv[1].isdigit() else 14
    load_env()
    rows = fetch_chat_supabase(days)
    if not rows:
        print("Keine Chat-Daten — Abbruch (kein Report, kein State-Write).")
        sys.exit(1)
    candidates = mine_candidates(rows)
    seen = load_seen()
    fresh = [c for c in candidates if c["key"] not in seen]
    if fresh:
        append_candidates(fresh)
        seen.update(c["key"] for c in fresh)
        save_seen(seen)
    report = write_report(fresh, len(rows), days)
    print(f"gescannt={len(rows)} kandidaten={len(candidates)} neu={len(fresh)}")
    print(f"report={report}")
    if fresh:
        print(f"candidates-file={CANDIDATES_FILE}")


if __name__ == "__main__":
    main()
