#!/usr/bin/env python3
"""Aria Insight-Harvest (KAR-588) — periodischer Re-Surface- und Action-Loop ueber
das GESAMTE Insight-Register (aria-insight-register.db).

Wasserdicht: scannt ALLE Items (nicht nur neue), matcht „later"-Funde gegen den
aktuellen Arbeits-Kontext (aktive KARs), findet Cross-Patterns (>=3 Quellen zum
selben Thema = Signal), re-challenged alte Items und produziert einen Report +
Telegram-Kurzfassung. Coverage-Check garantiert: kein Item wird uebersprungen.

Aufruf:
  python3 aria-insight-harvest.py [--no-telegram] [--top N] [--report-only]
"""
from __future__ import annotations
import argparse
import os
import re
import sqlite3
import sys
import urllib.parse
import urllib.request
import json
from collections import Counter, defaultdict
from datetime import datetime, timezone, timedelta
from pathlib import Path

DB = Path("/root/aria/state/insight-register.db")
BRAIN = Path("/root/aria/brain")
REPORT_DIR = BRAIN / "02-Wissen" / "insight-harvest-reports"
CHAT_ID = "1164395546"
NOW = datetime.now(timezone.utc)
STOP = {"der","die","das","und","fuer","mit","von","the","and","for","with","aria","kais",
        "video","note","2026","claude","kar","sub","check","feature","init","add","master"}


# ---------- Telegram (pattern from aria-akp-briefing) ----------
def get_tg_token():
    for path in ["/root/.claude/channels/telegram/.env", "/root/aria/.env", "/root/.env"]:
        p = Path(path)
        if not p.exists():
            continue
        for line in p.read_text().splitlines():
            line = line.strip()
            if "=" in line and not line.startswith("#"):
                k, _, v = line.partition("=")
                if k.strip() in ("TG_TOKEN","TELEGRAM_BOT_TOKEN","TELEGRAM_TOKEN") and v.strip():
                    return v.strip().strip('"').strip("'")
    return os.environ.get("TG_TOKEN")


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


def send_tg(text: str) -> bool:
    tok = get_tg_token()
    if not tok:
        print("[harvest] no telegram token", file=sys.stderr); 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"[harvest] tg error: {e}", file=sys.stderr); return False


# ---------- Active context (Linear) ----------
def load_active_context() -> Counter:
    """Keywords aus aktiven KARs (In Progress/Todo/Urgent/High Backlog) als Kontext."""
    key = os.environ.get("LINEAR_API_KEY")
    kw = Counter()
    if not key:
        return kw
    H = {"Authorization": key, "Content-Type": "application/json"}
    open_types = {"backlog", "unstarted", "started", "triage"}

    def post(query, variables=None):
        r = urllib.request.Request("https://api.linear.app/graphql",
            data=json.dumps({"query": query, "variables": variables or {}}).encode(),
            headers=H, method="POST")
        with urllib.request.urlopen(r, timeout=20) as resp:
            return json.loads(resp.read())

    try:
        d = post("{ teams(first:50){ nodes{ id key } } }")
        tid = next(t["id"] for t in d["data"]["teams"]["nodes"] if t["key"] == "KAR")
        cursor = None
        while True:
            q = '''query($id:String!,$c:String){ team(id:$id){ issues(first:100, after:$c){ pageInfo{hasNextPage endCursor} nodes{ title priority state{type} } } } }'''
            d = post(q, {"id": tid, "c": cursor})
            page = d["data"]["team"]["issues"]
            for i in page["nodes"]:
                st = i["state"]["type"]
                if st not in open_types:
                    continue
                w = 3 if st == "started" else (2 if (i["priority"] or 5) <= 2 else 1)
                for tok in re.findall(r"[A-Za-z][A-Za-z0-9-]{3,}", i["title"].lower()):
                    if tok not in STOP:
                        kw[tok] += w
            if page["pageInfo"]["hasNextPage"]:
                cursor = page["pageInfo"]["endCursor"]
            else:
                break
    except Exception as e:
        print(f"[harvest] linear ctx error: {e}", file=sys.stderr)
    return kw


# ---------- Harvest ----------
def harvest(top: int, mark_reviewed: bool):
    conn = sqlite3.connect(DB)
    rows = conn.execute(
        "SELECT id, source_path, kind, title, status, future_relevance, trigger_note, last_reviewed FROM insights"
    ).fetchall()
    total = len(rows)
    ctx = load_active_context()
    ctx_top = [w for w, _ in ctx.most_common(40)]

    scanned = 0
    resurface = []       # (score, matched_tags, row)
    tag_counter = Counter()
    challenge = []       # old never-reviewed later items

    for (iid, sp, kind, title, status, fr, trig, last_rev) in rows:
        scanned += 1
        tags = [t.strip().lower() for t in (fr or "").split(",") if t.strip()]
        for t in tags:
            tag_counter[t] += 1
        if status in ("implemented", "rejected"):
            continue
        # context match
        matched = [t for t in tags if t in ctx_top]
        if matched:
            score = sum(ctx[t] for t in matched)
            resurface.append((score, matched, (iid, sp, kind, title, status)))
        # re-challenge: later items never reviewed or reviewed >28d ago
        if status in ("later", "new"):
            stale = True
            if last_rev:
                try:
                    stale = (NOW - datetime.fromisoformat(last_rev)).days > 28
                except Exception:
                    stale = True
            if stale:
                challenge.append((iid, kind, title, status))

    # cross-pattern: tags appearing in >=3 items
    signals = [(t, c) for t, c in tag_counter.most_common(30) if c >= 3 and t in ctx_top]
    resurface.sort(key=lambda x: -x[0])

    coverage_ok = (scanned == total)

    # mark reviewed
    if mark_reviewed:
        conn.execute("UPDATE insights SET last_reviewed=? WHERE status IN ('later','new')", (NOW.isoformat(),))
        conn.commit()

    # status breakdown
    bd = dict(conn.execute("SELECT status, COUNT(*) FROM insights GROUP BY status").fetchall())
    impl = bd.get("implemented", 0)
    conn.close()

    return {
        "total": total, "scanned": scanned, "coverage_ok": coverage_ok,
        "resurface": resurface[:top], "signals": signals[:8],
        "challenge_n": len(challenge), "breakdown": bd, "impl": impl,
        "ctx_top": ctx_top[:12],
    }


def write_report(h: dict) -> Path:
    REPORT_DIR.mkdir(parents=True, exist_ok=True)
    fn = REPORT_DIR / f"harvest-{NOW.strftime('%Y-%m-%d')}.md"
    lines = [
        f"# Insight-Harvest — {NOW.strftime('%Y-%m-%d %H:%M UTC')}",
        "",
        f"- Register total: {h['total']} · gescannt: {h['scanned']} · "
        f"Coverage: {'OK ✓' if h['coverage_ok'] else 'FEHLER — Items uebersprungen!'}",
        f"- Status: {h['breakdown']}",
        f"- Umsetzungs-Quote: {h['impl']}/{h['total']}",
        f"- Aktiver Kontext (Top-Keywords aus aktiven KARs): {', '.join(h['ctx_top'])}",
        "",
        "## Re-Surface — jetzt relevante alte Funde",
        "",
    ]
    if h["resurface"]:
        for score, matched, (iid, sp, kind, title, status) in h["resurface"]:
            lines.append(f"- **[{kind}]** {title}  \n  matched: `{', '.join(matched)}` (score {score}) · status {status} · `{sp}`")
    else:
        lines.append("- (keine Matches gegen aktuellen Kontext)")
    lines += ["", "## Cross-Pattern-Signale (>=3 Quellen, kontext-relevant)", ""]
    if h["signals"]:
        for t, c in h["signals"]:
            lines.append(f"- `{t}` — {c} Funde im Register")
    else:
        lines.append("- (keine)")
    lines += ["", f"## Re-Challenge: {h['challenge_n']} later-Items >28d nicht reviewed", "",
              "(beim naechsten Lauf als reviewed markiert; aufgenommen ins Re-Surface wenn Kontext matcht)", ""]
    fn.write_text("\n".join(lines))
    return fn


def build_tg(h: dict, report: Path) -> str:
    L = ["*Insight\\-Harvest* " + md2(NOW.strftime("%Y-%m-%d")), ""]
    cov = "OK ✓" if h["coverage_ok"] else "🔴 FEHLER"
    L.append(f"*Register:* {h['total']} Funde · Coverage *{md2(cov)}* · Umsetzung {h['impl']}/{h['total']}")
    L.append("")
    if h["resurface"]:
        L.append("*Jetzt relevant geworden \\(Re\\-Surface\\):*")
        for score, matched, (iid, sp, kind, title, status) in h["resurface"][:5]:
            L.append(f"• \\[{md2(kind)}\\] {md2(title[:55])} — `{md2(', '.join(matched[:3]))}`")
        L.append("")
    else:
        L.append("*Re\\-Surface:* keine alten Funde matchen den aktuellen Kontext\\.")
        L.append("")
    if h["signals"]:
        L.append("*Cross\\-Pattern\\-Signale:*")
        for t, c in h["signals"][:5]:
            L.append(f"• `{md2(t)}` — {c} Quellen")
        L.append("")
    L.append(f"_{h['challenge_n']} alte Items re\\-challenged\\. Report: {md2(report.name)}_")
    return "\n".join(L)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--no-telegram", action="store_true")
    ap.add_argument("--report-only", action="store_true")
    ap.add_argument("--top", type=int, default=10)
    args = ap.parse_args()
    if not DB.exists():
        print("[harvest] register db missing — run aria-insight-register.py ingest first")
        return 1
    h = harvest(args.top, mark_reviewed=not args.report_only)
    report = write_report(h)
    print(f"[harvest] total={h['total']} scanned={h['scanned']} coverage={'OK' if h['coverage_ok'] else 'FAIL'} "
          f"resurface={len(h['resurface'])} signals={len(h['signals'])} → {report}")
    if not h["coverage_ok"]:
        print("[harvest] COVERAGE FAILURE — some items skipped!", file=sys.stderr)
    if not args.no_telegram:
        send_tg(build_tg(h, report))
    return 0


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