#!/usr/bin/env python3
"""Aria Rule-Challenge (KAR-590) — LLM-as-Judge Compliance-Challenge.

Haelt jede Standing-Order regelmaessig gegen die ECHTE Aria-Praxis (letzte Outputs):
„Wurde die Regel befolgt oder verletzt? Belege? Ist sie noch gueltig oder tot?"

Wasserdicht via Round-Robin: ein State-File trackt wann jede Regel zuletzt gechallengt
wurde; jeder Lauf nimmt die N am laengsten nicht geprueften → ueber die Zeit kommt
GARANTIERT jede Regel dran, nichts wird ausgelassen.

Aufruf:
  python3 aria-rule-challenge.py [--n 12] [--days 7] [--no-telegram] [--dry-run]
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
import urllib.parse
import urllib.request
from datetime import datetime, timezone, timedelta
from pathlib import Path

MEMORY = Path("/root/.claude/projects/-/memory")
STATE = Path("/root/aria/state/rule-challenge.json")
REPORT_DIR = Path("/root/aria/brain/02-Wissen/rule-challenge-reports")
CHAT_ID = "1164395546"
HAIKU = "claude-haiku-4-5-20251001"
NOW = datetime.now(timezone.utc)


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


def load_rules() -> list[dict]:
    rules = []
    for p in sorted(MEMORY.glob("feedback_*.md")):
        text = p.read_text(errors="replace")
        desc = ""
        m = re.search(r"^description:\s*(.+)$", text, re.M)
        if m:
            desc = m.group(1).strip().strip('"').strip("'")
        body = re.sub(r"^---.*?---", "", text, count=1, flags=re.S).strip()
        rules.append({"slug": p.stem, "desc": desc, "body": body[:1200]})
    return rules


def load_state() -> dict:
    if STATE.exists():
        try:
            return json.loads(STATE.read_text())
        except Exception:
            pass
    return {"last_challenged": {}}


def save_state(s: dict):
    STATE.parent.mkdir(parents=True, exist_ok=True)
    STATE.write_text(json.dumps(s, indent=2))


def pick_rotation(rules, state, n):
    lc = state.get("last_challenged", {})
    # never-challenged first (epoch 0), then oldest
    rules_sorted = sorted(rules, key=lambda r: lc.get(r["slug"], "1970-01-01"))
    return rules_sorted[:n]


def fetch_recent_outputs(days: int) -> str:
    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:
        return ""
    cutoff = (NOW - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
    req_url = (f"{url}/rest/v1/aria_chat_log?select=created_at,direction,message_text"
               f"&direction=eq.out&created_at=gte.{cutoff}&order=created_at.desc&limit=400")
    try:
        req = urllib.request.Request(req_url, headers={"apikey": key, "Authorization": f"Bearer {key}"})
        with urllib.request.urlopen(req, timeout=20) as r:
            rows = json.loads(r.read())
        # concat, cap to keep prompt cost low
        txt = "\n---\n".join((x.get("message_text") or "")[:600] for x in rows)
        return txt[:18000]
    except Exception as e:
        print(f"[rule-challenge] supabase err: {e}", file=sys.stderr)
        return ""


def challenge_rule(client, rule: dict, outputs: str) -> dict:
    sys_prompt = (
        "Du bist ein harter, ehrlicher Compliance-Auditor fuer Aria (KI-Agent). Du bekommst EINE "
        "Standing-Order-Regel und eine Stichprobe von Arias echten Outputs der letzten Tage. "
        "Bewerte NUR anhand der Belege, nicht wohlwollend. Antworte mit JSON:\n"
        '{"verdict":"COMPLIANT|VIOLATED|STALE|UNCLEAR","evidence":"<konkreter Beleg/Zitat oder \'keine Daten\'>",'
        '"why":"<bei VIOLATED: kurze Root-Cause>","action":"<hook-kandidat|soul-promotion|archivieren|keine>"}\n'
        "COMPLIANT=befolgt, VIOLATED=nachweislich verletzt, STALE=Regel wirkt veraltet/nie relevant, "
        "UNCLEAR=zu wenig Daten ODER Regel im Zeitraum nicht anwendbar (z.B. API-Regel aber keine API-Arbeit). "
        "WICHTIG: 'nicht anwendbar / kein relevanter Kontext' ist UNCLEAR, NIEMALS VIOLATED. "
        "VIOLATED nur bei konkretem Beleg dass eine ANWENDBARE Regel missachtet wurde. "
        "Sei streng bei Einhaltung (ohne Beleg eher UNCLEAR als COMPLIANT), aber fair bei Verstoss (kein Verstoss ohne anwendbaren Kontext)."
    )
    usr = (f"REGEL ({rule['slug']}):\n{rule['desc']}\n\nDETAIL:\n{rule['body']}\n\n"
           f"ARIA-OUTPUTS (Stichprobe letzte Tage):\n{outputs or '(keine Output-Daten verfuegbar)'}")
    try:
        resp = client.messages.create(model=HAIKU, max_tokens=600,
                                      system=sys_prompt, messages=[{"role": "user", "content": usr}])
        text = "".join(b.text for b in resp.content if hasattr(b, "text")).strip()
        if "```" in text:  # strip code fences
            parts = text.split("```")
            text = max(parts, key=len)
            if text.lstrip().startswith("json"):
                text = text.lstrip()[4:]
        first, last = text.find("{"), text.rfind("}")
        if first != -1 and last != -1:
            return json.loads(text[first:last + 1])
        return {"verdict": "UNCLEAR", "evidence": f"[parse-fail: {text[:120]}]", "why": "", "action": "keine"}
    except Exception as e:
        return {"verdict": "UNCLEAR", "evidence": f"[error: {e}]", "why": "", "action": "keine"}


def md2(s): return re.sub(r"([_*\[\]()~`>#+\-=|{}.!\\])", r"\\\1", str(s))


def send_tg(text):
    tok = os.environ.get("TG_TOKEN") or os.environ.get("TELEGRAM_BOT_TOKEN")
    if not tok:
        return False
    data = urllib.parse.urlencode({"chat_id": CHAT_ID, "text": text,
        "parse_mode": "MarkdownV2", "disable_web_page_preview": "true"}).encode()
    try:
        with urllib.request.urlopen(urllib.request.Request(
                f"https://api.telegram.org/bot{tok}/sendMessage", data=data, method="POST"), timeout=15) as r:
            return r.status == 200
    except Exception as e:
        print(f"[rule-challenge] tg err: {e}", file=sys.stderr); return False


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--n", type=int, default=12)
    ap.add_argument("--days", type=int, default=7)
    ap.add_argument("--no-telegram", action="store_true")
    ap.add_argument("--dry-run", action="store_true")
    args = ap.parse_args()
    load_env()

    rules = load_rules()
    state = load_state()
    batch = pick_rotation(rules, state, args.n)

    if args.dry_run:
        print(f"{len(rules)} rules total. Would challenge {len(batch)}:")
        for r in batch:
            print(f"  {r['slug']} (last: {state['last_challenged'].get(r['slug'],'never')})")
        return 0

    api_key = os.environ.get("ANTHROPIC_API_KEY")
    if not api_key:
        print("[rule-challenge] ANTHROPIC_API_KEY missing"); return 1
    from anthropic import Anthropic
    client = Anthropic(api_key=api_key)

    outputs = fetch_recent_outputs(args.days)
    results = []
    for r in batch:
        v = challenge_rule(client, r, outputs)
        results.append((r["slug"], v))
        state["last_challenged"][r["slug"]] = NOW.isoformat()

    save_state(state)

    # coverage: how many of all rules challenged within last 28d
    lc = state["last_challenged"]
    covered = sum(1 for r in rules if lc.get(r["slug"]) and
                  (NOW - datetime.fromisoformat(lc[r["slug"]])).days <= 28)
    violated = [(s, v) for s, v in results if v.get("verdict") == "VIOLATED"]
    stale = [(s, v) for s, v in results if v.get("verdict") == "STALE"]

    # report
    REPORT_DIR.mkdir(parents=True, exist_ok=True)
    fn = REPORT_DIR / f"rule-challenge-{NOW.strftime('%Y-%m-%d')}.md"
    lines = [f"# Rule-Challenge — {NOW.strftime('%Y-%m-%d %H:%M UTC')}", "",
             f"- Geprueft: {len(batch)}/{len(rules)} Regeln (Round-Robin)",
             f"- Coverage 28d: {covered}/{len(rules)} Regeln gechallengt",
             f"- VIOLATED: {len(violated)} · STALE: {len(stale)}", ""]
    for s, v in results:
        lines.append(f"## {s} — **{v.get('verdict')}**")
        lines.append(f"- Beleg: {v.get('evidence','')}")
        if v.get("why"):
            lines.append(f"- Root-Cause: {v['why']}")
        if v.get("action") and v["action"] != "keine":
            lines.append(f"- Aktion: {v['action']}")
        lines.append("")
    fn.write_text("\n".join(lines))
    print(f"[rule-challenge] {len(batch)} challenged, {len(violated)} VIOLATED, {len(stale)} STALE, coverage {covered}/{len(rules)} → {fn}")

    if not args.no_telegram:
        L = [f"*Rule\\-Challenge* {md2(NOW.strftime('%Y-%m-%d'))}", "",
             f"Geprueft {len(batch)}/{len(rules)} · Coverage 28d *{covered}/{len(rules)}*", ""]
        if violated:
            L.append("🔴 *VERLETZT:*")
            for s, v in violated[:6]:
                L.append(f"• `{md2(s.replace('feedback_',''))}` — {md2(v.get('evidence','')[:80])}")
            L.append("")
        if stale:
            L.append("🟠 *STALE \\(Archiv\\-Kandidat\\):*")
            for s, v in stale[:4]:
                L.append(f"• `{md2(s.replace('feedback_',''))}`")
            L.append("")
        if not violated and not stale:
            L.append("✅ keine Verstoesse/stale in dieser Runde")
        send_tg("\n".join(L))
    return 0


if __name__ == "__main__":
    sys.exit(main())
