#!/usr/bin/env python3
"""Prompt quality verification — all 14 LLMClient task methods with real Gemini.

Calls each task method with realistic input data, prints formatted
input/output for HUMAN evaluation. Not automated pass/fail — the purpose
is to see what each prompt produces and judge quality.

Usage:
    cd ~/neo-ram/neo-cortex
    CORTEX_DATA_DIR=~/neo-ram uv run python tests/integration/test_prompt_quality.py
    CORTEX_DATA_DIR=~/neo-ram uv run python tests/integration/test_prompt_quality.py --only classify,distill
    CORTEX_DATA_DIR=~/neo-ram uv run python tests/integration/test_prompt_quality.py --provider groq
"""

import argparse
import asyncio
import json
import os
import sys
import time

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))

# --- Realistic test data ---

CONVERSATION_TURN = """user: Come faccio a fixare il CORS nel neo-reloaded? Le richieste dall'estensione VS Code vengono bloccate.

assistant: Il problema è che manca il middleware CORS nella pipeline. Devi aggiungere `app.UseCors()` PRIMA di `UseRouting()` in Program.cs. Il fix è semplice:

```csharp
app.UseCors(policy => policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
app.UseRouting();
```

Questo è necessario perché VS Code extension fa richieste cross-origin al backend neo-reloaded sulla porta 5070. Senza CORS middleware, il browser blocca tutte le richieste."""

CLASSIFY_INPUT = (
    "Q: Come faccio a fixare il CORS nel neo-reloaded?\n"
    "A: Devi aggiungere app.UseCors() prima di UseRouting() in Program.cs. "
    "Il middleware CORS mancava dalla pipeline."
)

NOISE_INPUTS = [
    "ciao",
    "ok grazie",
    "Come funziona il SimHash dedup in neo-cortex?",
    "rifai il deploy su systemd",
]

QUERY_INPUT = "recent bugs in neo-reloaded engine startup"

CONFLICT_EXISTING = {
    "title": "CORS fix in neo-reloaded",
    "summary": "Added app.UseCors() before UseRouting() in Program.cs",
    "facts": '["CORS middleware was missing", "Fix: add UseCors() before UseRouting()"]',
}
CONFLICT_NEW_DUPLICATE = (
    "CORS was blocking VS Code extension requests. Fixed by adding "
    "app.UseCors() middleware before UseRouting() in Program.cs."
)
CONFLICT_NEW_DIFFERENT = (
    "SimHash deduplication uses 64-bit hashes with hamming distance "
    "threshold of 10 bits to detect near-duplicate memories."
)

AGGREGATE_MEMORIES = """[1] CORS fix in neo-reloaded
Added app.UseCors() before UseRouting() in Program.cs to fix VS Code extension requests being blocked.

[2] SimHash deduplication threshold
SimHash uses 64-bit hashes with hamming distance threshold of 10 bits. Catches near-duplicate memories before storage.

[3] Execution-day Ebbinghaus decay
Memory decay uses execution days not calendar days. Formula: exp(-(days/10)/stability) with floor 0.05. This prevents memories from dying when project is paused."""

MERGE_MEMORIES = """[Memory 1] id=kn_001
Title: CORS fix applied
Content: Added app.UseCors() before UseRouting() in Program.cs to fix cross-origin requests.

[Memory 2] id=kn_002
Title: CORS middleware missing
Content: VS Code extension requests were blocked because CORS middleware was missing from the pipeline. Fix: add UseCors() before UseRouting()."""

FOCAL_POINTS_INPUT = """- [m1] Fixed CORS bug by reading error message first, then tracing to missing middleware
- [m2] Wrote 15 tests before implementing ConceptGraph feature
- [m3] Debugged hanging digestor by isolating the asyncio.sleep mock issue
- [m4] Decided to use execution-day decay after analyzing 3 alternative approaches
- [m5] Refactored LLM provider abstraction using plan-first approach"""

INSIGHT_EVIDENCE = """- [m1] Fixed CORS bug by reading error message first, then tracing to missing middleware
- [m3] Debugged hanging digestor by isolating the asyncio.sleep mock issue"""

TRUTHFULNESS_EVIDENCE = """- [m1] Fixed CORS bug by reading error message first
- [m3] Debugged hanging digestor by isolating the issue step by step
- [m5] Used Protocol D to trace a crash in EngineHost"""

IDENTITY_INSIGHTS = """- [v3_meta_1] Agent consistently uses Protocol D for debugging (energy=0.9, stability=0.7)
- [v3_meta_2] Agent reads error messages before acting (energy=0.85, stability=0.65)
- [v3_meta_3] Agent isolates problems systematically before fixing (energy=0.8, stability=0.6)"""

COHERENCE_PROPOSAL = """SECTION: §DEBUG
PROPOSAL: @learned::systematicDebugging→protocolD+readError+isolate{evidence::#3insights}
REASON: Three consistent insights over multiple sessions confirm systematic debugging pattern."""

TIMELINE_MEMORIES = """[1] (neo-cortex/feature) ConceptGraph spreading activation
Added NetworkX-based concept graph with spreading activation for recall boost. 2-hop traversal with decay 0.5.

[2] (neo-cortex/bugfix) SimHash dedup threshold tuning
Changed hamming distance threshold from 5 to 10 bits. Was too aggressive — blocking legitimate similar memories.

[3] (neo-reloaded/feature) Synapse sensor manifest protocol
Designed SMP v1.0 for self-describing sensors. Register → SendStimulus → OnResponse flow via SignalR."""


def load_keys():
    keys_file = os.path.expanduser("~/neo-ram/.keys")
    if not os.path.exists(keys_file):
        return
    mapping = {
        "gemini": "GEMINI_API_KEY",
        "groq": "GROQ_API_KEY",
        "open-ai": "OPENAI_API_KEY",
        "anthropic": "ANTHROPIC_API_KEY",
        "z.ai": "ZAI_API_KEY",
        "jina": "JINA_API_KEY",
    }
    with open(keys_file) as f:
        for line in f:
            if ":" in line:
                name, key = line.strip().split(":", 1)
                env_var = mapping.get(name.strip())
                if env_var and env_var not in os.environ:
                    os.environ[env_var] = key.strip()


def header(name, num):
    print(f"\n{'='*70}")
    print(f"  [{num}/14] {name}")
    print(f"{'='*70}")


def show(label, text, max_lines=20):
    lines = str(text).strip().split("\n")
    truncated = lines[:max_lines]
    print(f"\n  {label}:")
    for line in truncated:
        print(f"    {line}")
    if len(lines) > max_lines:
        print(f"    ... ({len(lines) - max_lines} more lines)")


async def run_all(provider: str, only: set | None):
    from neo_cortex.llm import create_client

    if "CORTEX_DATA_DIR" not in os.environ:
        os.environ["CORTEX_DATA_DIR"] = os.path.expanduser("~/neo-ram")

    llm = create_client(provider)
    print(f"\n  Provider: {llm.name}")
    print(f"  Testing all 13 task methods with realistic data\n")

    results = {}

    # 1. classify
    if not only or "classify" in only:
        header("classify", 1)
        show("INPUT", CLASSIFY_INPUT)
        t0 = time.monotonic()
        out = await llm.classify(CLASSIFY_INPUT)
        ms = (time.monotonic() - t0) * 1000
        show(f"OUTPUT ({ms:.0f}ms)", out)
        try:
            data = json.loads(out)
            print(f"\n  PARSE: OK — project={data.get('project')}, topic={data.get('topic')}, "
                  f"activity={data.get('activity')}, concepts={data.get('concepts', [])[:4]}")
            results["classify"] = "OK"
        except json.JSONDecodeError:
            print(f"\n  PARSE: FAILED — not valid JSON")
            results["classify"] = "FAIL"

    # 2. check_noise
    if not only or "noise" in only:
        header("check_noise", 2)
        for inp in NOISE_INPUTS:
            show("INPUT", inp)
            t0 = time.monotonic()
            out = await llm.check_noise(inp)
            ms = (time.monotonic() - t0) * 1000
            show(f"OUTPUT ({ms:.0f}ms)", out)
            try:
                data = json.loads(out)
                print(f"  PARSE: noise={data.get('noise')}, why={data.get('why')}")
            except json.JSONDecodeError:
                print(f"  PARSE: FAILED")
        results["noise"] = "OK"

    # 3. analyze_query
    if not only or "query" in only:
        header("analyze_query", 3)
        show("INPUT", QUERY_INPUT)
        t0 = time.monotonic()
        out = await llm.analyze_query(QUERY_INPUT)
        ms = (time.monotonic() - t0) * 1000
        show(f"OUTPUT ({ms:.0f}ms)", out)
        try:
            data = json.loads(out)
            print(f"\n  PARSE: OK — project={data.get('project')}, refined={data.get('refined_query')}")
            results["query"] = "OK"
        except json.JSONDecodeError:
            print(f"\n  PARSE: FAILED")
            results["query"] = "FAIL"

    # 4. resolve_conflict (duplicate)
    if not only or "conflict" in only:
        header("resolve_conflict (duplicate input)", 4)
        show("EXISTING", f"Title: {CONFLICT_EXISTING['title']}\nSummary: {CONFLICT_EXISTING['summary']}")
        show("NEW (duplicate)", CONFLICT_NEW_DUPLICATE)
        t0 = time.monotonic()
        out = await llm.resolve_conflict(
            CONFLICT_NEW_DUPLICATE,
            CONFLICT_EXISTING["title"],
            CONFLICT_EXISTING["summary"],
            CONFLICT_EXISTING["facts"],
        )
        ms = (time.monotonic() - t0) * 1000
        show(f"OUTPUT ({ms:.0f}ms)", out)
        try:
            data = json.loads(out)
            action = data.get("action")
            print(f"\n  PARSE: action={action}, reason={data.get('reason')}")
            print(f"  EXPECTED: noop or update (it's a duplicate)")
            results["conflict_dup"] = "OK" if action in ("noop", "update") else "WRONG"
        except json.JSONDecodeError:
            print(f"\n  PARSE: FAILED")
            results["conflict_dup"] = "FAIL"

        # Different topic
        show("NEW (different topic)", CONFLICT_NEW_DIFFERENT)
        t0 = time.monotonic()
        out = await llm.resolve_conflict(
            CONFLICT_NEW_DIFFERENT,
            CONFLICT_EXISTING["title"],
            CONFLICT_EXISTING["summary"],
            CONFLICT_EXISTING["facts"],
        )
        ms = (time.monotonic() - t0) * 1000
        show(f"OUTPUT ({ms:.0f}ms)", out)
        try:
            data = json.loads(out)
            action = data.get("action")
            print(f"\n  PARSE: action={action}, reason={data.get('reason')}")
            print(f"  EXPECTED: add (different topic)")
            results["conflict_diff"] = "OK" if action == "add" else "WRONG"
        except json.JSONDecodeError:
            print(f"\n  PARSE: FAILED")
            results["conflict_diff"] = "FAIL"

    # 5. aggregate
    if not only or "aggregate" in only:
        header("aggregate (MBEL compression)", 5)
        show("INPUT", AGGREGATE_MEMORIES)
        t0 = time.monotonic()
        out = await llm.aggregate(AGGREGATE_MEMORIES)
        ms = (time.monotonic() - t0) * 1000
        show(f"OUTPUT ({ms:.0f}ms)", out)
        has_mbel = any(op in out for op in ["@", "→", "::", "✓", "⚡"])
        print(f"\n  MBEL operators present: {has_mbel}")
        results["aggregate"] = "OK" if has_mbel else "WEAK"

    # 7. distill
    if not only or "distill" in only:
        header("distill (knowledge extraction)", 6)
        show("INPUT", CONVERSATION_TURN)
        t0 = time.monotonic()
        out = await llm.distill(CONVERSATION_TURN)
        ms = (time.monotonic() - t0) * 1000
        show(f"OUTPUT ({ms:.0f}ms)", out)
        try:
            start = out.find("[")
            end = out.rfind("]")
            if start >= 0 and end > start:
                entries = json.loads(out[start:end+1])
                print(f"\n  PARSE: {len(entries)} entries extracted")
                for e in entries:
                    print(f"    [{e.get('type')}] {e.get('topic')} — concepts: {e.get('concepts', [])}")
                results["distill"] = "OK"
            else:
                print(f"\n  PARSE: no JSON array found")
                results["distill"] = "FAIL"
        except json.JSONDecodeError:
            print(f"\n  PARSE: FAILED")
            results["distill"] = "FAIL"

    # 8. merge_memories
    if not only or "merge" in only:
        header("merge_memories", 7)
        show("INPUT", MERGE_MEMORIES)
        t0 = time.monotonic()
        out = await llm.merge_memories(MERGE_MEMORIES)
        ms = (time.monotonic() - t0) * 1000
        show(f"OUTPUT ({ms:.0f}ms)", out)
        try:
            data = json.loads(out)
            print(f"\n  PARSE: action={data.get('action')}, "
                  f"title={data.get('merged_title', 'N/A')}")
            print(f"  EXPECTED: merge (same CORS topic)")
            results["merge"] = "OK" if data.get("action") == "merge" else "WRONG"
        except json.JSONDecodeError:
            print(f"\n  PARSE: FAILED")
            results["merge"] = "FAIL"

    # 9. focal_points
    if not only or "focal" in only:
        header("focal_points (metacognitive)", 8)
        show("INPUT", FOCAL_POINTS_INPUT)
        t0 = time.monotonic()
        out = await llm.focal_points(FOCAL_POINTS_INPUT)
        ms = (time.monotonic() - t0) * 1000
        show(f"OUTPUT ({ms:.0f}ms)", out)
        lines = [l for l in out.strip().split("\n") if l.strip()]
        has_questions = sum(1 for l in lines if "?" in l)
        print(f"\n  Questions found: {has_questions}")
        results["focal"] = "OK" if has_questions >= 2 else "WEAK"

    # 10. generate_insight
    if not only or "insight" in only:
        header("generate_insight", 9)
        show("FOCAL POINT", "How does the agent approach debugging?")
        show("EVIDENCE", INSIGHT_EVIDENCE)
        t0 = time.monotonic()
        out = await llm.generate_insight("How does the agent approach debugging?", INSIGHT_EVIDENCE)
        ms = (time.monotonic() - t0) * 1000
        show(f"OUTPUT ({ms:.0f}ms)", out)
        has_insight = "INSIGHT:" in out.upper() or "insight" in out.lower()
        has_evidence = "EVIDENCE:" in out.upper() or "m1" in out or "m3" in out
        print(f"\n  Has INSIGHT line: {has_insight}, Has EVIDENCE refs: {has_evidence}")
        results["insight"] = "OK" if has_insight else "WEAK"

    # 11. verify_truthfulness
    if not only or "truth" in only:
        header("verify_truthfulness", 10)
        claim = "The agent consistently uses Protocol D for systematic debugging"
        show("CLAIM", claim)
        show("EVIDENCE", TRUTHFULNESS_EVIDENCE)
        t0 = time.monotonic()
        out = await llm.verify_truthfulness(claim, TRUTHFULNESS_EVIDENCE)
        ms = (time.monotonic() - t0) * 1000
        show(f"OUTPUT ({ms:.0f}ms)", out)
        starts_supported = out.strip().upper().startswith("SUPPORTED")
        starts_unsupported = out.strip().upper().startswith("UNSUPPORTED")
        print(f"\n  Starts with SUPPORTED: {starts_supported}")
        print(f"  Starts with UNSUPPORTED: {starts_unsupported}")
        print(f"  EXPECTED: UNSUPPORTED (only 1/3 evidence explicitly names Protocol D)")
        has_valid_format = starts_supported or starts_unsupported
        results["truth"] = "OK" if has_valid_format else "FAIL"

    # 12. identity_proposal
    if not only or "identity" in only:
        header("identity_proposal", 11)
        show("INPUT (mature insights)", IDENTITY_INSIGHTS)
        t0 = time.monotonic()
        out = await llm.identity_proposal(IDENTITY_INSIGHTS)
        ms = (time.monotonic() - t0) * 1000
        show(f"OUTPUT ({ms:.0f}ms)", out)
        has_section = "SECTION:" in out.upper()
        has_proposal = "PROPOSAL:" in out.upper()
        has_reason = "REASON:" in out.upper()
        print(f"\n  Has SECTION: {has_section}, PROPOSAL: {has_proposal}, REASON: {has_reason}")
        results["identity"] = "OK" if has_section and has_proposal else "WEAK"

    # 13. coherence_check
    if not only or "coherence" in only:
        header("coherence_check", 12)
        show("INPUT (proposal)", COHERENCE_PROPOSAL)
        t0 = time.monotonic()
        out = await llm.coherence_check(COHERENCE_PROPOSAL)
        ms = (time.monotonic() - t0) * 1000
        show(f"OUTPUT ({ms:.0f}ms)", out)
        starts_coherent = out.strip().upper().startswith("COHERENT")
        print(f"\n  Starts with COHERENT: {starts_coherent}")
        print(f"  EXPECTED: COHERENT (aligns with §DEBUG)")
        results["coherence"] = "OK" if starts_coherent else "WRONG"

    # 14. timeline_resume (via aggregate as proxy — timeline uses httpx directly)
    if not only or "timeline" in only:
        header("timeline_resume (via raw call)", 13)
        from neo_cortex.prompts import DEFAULT_PROMPTS
        from neo_cortex.utils import load_mbel_grammar
        grammar = load_mbel_grammar()
        prompt = DEFAULT_PROMPTS["timeline_resume"].format(
            grammar=grammar, memories_text=TIMELINE_MEMORIES,
        )
        show("INPUT (3 memories)", TIMELINE_MEMORIES)
        t0 = time.monotonic()
        out = await llm.call(prompt, "", max_tokens=1200, temperature=0.2)
        ms = (time.monotonic() - t0) * 1000
        show(f"OUTPUT ({ms:.0f}ms)", out)
        has_mbel = any(op in out for op in ["§", "@", "→", "::", "✓"])
        print(f"\n  MBEL operators present: {has_mbel}")
        results["timeline"] = "OK" if has_mbel else "WEAK"

    await llm.close()

    # Summary
    print(f"\n{'='*70}")
    print(f"  SUMMARY — {llm.name}")
    print(f"{'='*70}")
    for name, status in results.items():
        color = "\033[32m" if status == "OK" else "\033[33m" if status == "WEAK" else "\033[31m"
        print(f"  {color}{status:6s}\033[0m  {name}")
    ok = sum(1 for s in results.values() if s == "OK")
    total = len(results)
    print(f"\n  {ok}/{total} OK")
    print(f"{'='*70}")


def main():
    parser = argparse.ArgumentParser(description="Prompt quality verification")
    parser.add_argument("--provider", default="gemini", help="LLM provider (default: gemini)")
    parser.add_argument("--only", help="Test only these prompts (comma-separated: classify,distill,...)")
    args = parser.parse_args()

    load_keys()
    if "CORTEX_DATA_DIR" not in os.environ:
        os.environ["CORTEX_DATA_DIR"] = os.path.expanduser("~/neo-ram")

    only = set(args.only.split(",")) if args.only else None
    asyncio.run(run_all(args.provider, only))


if __name__ == "__main__":
    main()
