#!/usr/bin/env python3
"""
Hermes Dojo — Refined Analyzer

A drop-in companion to monitor.py that works around the known state.db bug:
tool_name is NULL in tool-role messages, making monitor.py report 0 tool calls.

This script builds tool names from assistant messages' tool_calls JSON,
matches tool responses by sequential position within each session, and
uses per-tool semantic error detection to avoid false positives.

Usage:
    python3 refined_analyze.py                    # Analyze last 7 days
    python3 refined_analyze.py --days 14          # Custom range
    python3 refined_analyze.py --json             # JSON output
    python3 refined_analyze.py --save-snapshot    # Also save to tracker
"""
import json
import os
import re
import sqlite3
import sys
import time
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any

HERMES_HOME = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
DB_PATH = HERMES_HOME / "state.db"

# Patterns in user messages that indicate corrections/dissatisfaction
CORRECTION_PATTERNS = [
    (r"(?i)^no[,.\s]", "no"),
    (r"(?i)wrong", "wrong"),
    (r"(?i)not what I", "not what I"),
    (r"(?i)I meant", "I meant"),
    (r"(?i)that's not", "that's not"),
    (r"(?i)incorrect", "incorrect"),
    (r"(?i)try again", "try again"),
    (r"(?i)fix this|fix it", "fix"),
    (r"(?i)still (not|doesn't|isn't)", "still not"),
    (r"(?i)undo|revert", "undo/revert"),
]

# Patterns indicating user is repeatedly asking for something (skill gap signal)
REQUEST_PATTERNS = [
    (r"(?i)parse.*csv", "csv-parsing"),
    (r"(?i)format.*json", "json-formatting"),
    (r"(?i)convert.*pdf", "pdf-conversion"),
    (r"(?i)send.*email", "email-sending"),
    (r"(?i)create.*chart", "chart-creation"),
    (r"(?i)scrape.*web", "web-scraping"),
    (r"(?i)deploy", "deployment"),
    (r"(?i)docker", "docker-management"),
    (r"(?i)git.*commit", "git-operations"),
    (r"(?i)test.*unit|unit.*test", "unit-testing"),
    (r"(?i)database|sql|query", "database-operations"),
    (r"(?i)api.*call|fetch.*api|rest.*api", "api-integration"),
]


def is_terminal_error(content: str) -> bool:
    """
    Detect real terminal errors WITHOUT false positives from usage strings.
    
    Checks structured fields (exit_code, error) instead of raw text patterns.
    Ignores:
    - exit_code=2 with "usage:" (grep/sort/find/tar help text)
    - exit_code=1 from "grep pattern" with no matches
    """
    if not content:
        return False
    try:
        data = json.loads(content) if isinstance(content, str) else content
        if not isinstance(data, dict):
            return False
        if data.get("error") not in (None, "", "null"):
            return True
        exit_code = data.get("exit_code")
        if exit_code is not None and exit_code != 0:
            output = data.get("output", "")
            # exit_code 2 with "usage:" = grep/sort/find help, NOT an error
            if exit_code == 2 and any(
                output.strip().startswith(f"usage: {c}")
                for c in ["grep", "sort", "find", "sed", "awk", "tar", "git", "cp", "mv", "ls"]
            ):
                return False
            # exit_code 1 from "grep pattern" no matches = not an error
            if exit_code == 1 and "grep" in output[:200] and "usage:" not in output[:200]:
                return False
            return True
    except (json.JSONDecodeError, TypeError, AttributeError):
        pass
    return False


def analyze_sessions(days: int = 7, session_id: str = None) -> dict[str, Any]:
    """Analyze recent sessions with accurate per-tool metrics."""
    if not DB_PATH.exists():
        return {"error": f"Database not found at {DB_PATH}"}

    conn = sqlite3.connect(str(DB_PATH))
    conn.row_factory = sqlite3.Row

    cutoff = time.time() - (days * 86400)

    # Get sessions
    if session_id:
        sessions = conn.execute(
            "SELECT * FROM sessions WHERE id = ?", (session_id,)
        ).fetchall()
    else:
        sessions = conn.execute(
            "SELECT * FROM sessions WHERE started_at > ? ORDER BY started_at DESC",
            (cutoff,),
        ).fetchall()

    if not sessions:
        conn.close()
        return {
            "sessions_analyzed": 0,
            "message": "No sessions found in the specified time range.",
        }

    session_ids = [s["id"] for s in sessions]
    placeholders = ",".join("?" for _ in session_ids)

    # Get all messages for these sessions
    all_messages = conn.execute(
        f"SELECT * FROM messages WHERE session_id IN ({placeholders}) ORDER BY timestamp",
        session_ids,
    ).fetchall()
    all_messages = [dict(m) for m in all_messages]

    conn.close()

    # === Build tool_name map from assistant messages ===
    tool_total: Counter = Counter()
    per_session_calls: dict[str, list[tuple[str, str]]] = defaultdict(list)

    for msg in all_messages:
        if msg["role"] == "assistant" and msg.get("tool_calls"):
            sid = msg["session_id"]
            calls_raw = msg["tool_calls"]
            try:
                calls = json.loads(calls_raw) if isinstance(calls_raw, str) else calls_raw
            except (json.JSONDecodeError, TypeError):
                continue
            for call in (calls if isinstance(calls, list) else [calls]):
                fn = call.get("function", {})
                name = fn.get("name", "")
                cid = call.get("call_id", "") or call.get("id", "")
                if name and cid:
                    tool_total[name] += 1
                    per_session_calls[sid].append((cid, name))

    total_calls_parsed = sum(tool_total.values())

    # === Match tool responses by position to detect errors ===
    tool_errors: Counter = Counter()
    tool_error_details: dict[str, list[str]] = defaultdict(list)
    all_sids = set(per_session_calls.keys()) | {m["session_id"] for m in all_messages}
    
    # Get only tool role messages
    tool_responses = [m for m in all_messages if m["role"] == "tool"]

    for sid in all_sids:
        calls = per_session_calls.get(sid, [])
        responses = [m for m in tool_responses if m["session_id"] == sid]
        for i, resp in enumerate(responses):
            tool_name = calls[i][1] if i < len(calls) else "UNKNOWN"
            content = resp.get("content") or ""
            is_err = False

            if tool_name == "terminal":
                is_err = is_terminal_error(content)
            else:
                try:
                    data = json.loads(content) if isinstance(content, str) else content
                    if isinstance(data, dict):
                        is_err = (
                            data.get("success") is False
                            or data.get("error") not in (None, "", "null")
                        )
                except (json.JSONDecodeError, TypeError, AttributeError):
                    pass

            if is_err:
                tool_errors[tool_name] += 1
                snippet = content[:200].replace("\n", " ")
                tool_error_details[tool_name].append(snippet)

    # === Analyze user corrections ===
    corrections_by_type = Counter()
    correction_samples = []
    for msg in all_messages:
        if msg["role"] == "user" and msg.get("content"):
            content = msg["content"]
            for pattern, ctype in CORRECTION_PATTERNS:
                if re.search(pattern, content):
                    corrections_by_type[ctype] += 1
                    if len(correction_samples) < 5:
                        correction_samples.append(content[:100].replace("\n", " "))
                    break

    # === Detect skill gaps ===
    skill_gaps = Counter()
    for msg in all_messages:
        if msg["role"] == "user" and msg.get("content"):
            for pattern, gap_name in REQUEST_PATTERNS:
                if re.search(pattern, msg["content"]):
                    skill_gaps[gap_name] += 1

    # === Detect retry patterns ===
    retries = _detect_retry_patterns(all_messages)

    # === Compute summary ===
    total_errors = sum(tool_errors.values())
    overall_success = (
        round((1 - total_errors / max(total_calls_parsed, 1)) * 100, 1)
        if total_calls_parsed > 0
        else 100.0
    )

    # Rank tools by failure rate
    weakest_tools = []
    for tool_name in sorted(tool_total, key=lambda t: tool_errors[t] / max(tool_total[t], 1), reverse=True):
        total = tool_total[tool_name]
        errs = tool_errors[tool_name]
        success_rate = round((1 - errs / max(total, 1)) * 100, 1)
        top_errors = tool_error_details.get(tool_name, [])
        weakest_tools.append({
            "tool": tool_name,
            "total": total,
            "errors": errs,
            "success_rate": success_rate,
            "top_error": top_errors[0][:120] if top_errors else "",
            "all_error_details": top_errors[:3],
        })

    # Sort by error rate descending
    weakest_tools.sort(key=lambda t: t["errors"] / max(t["total"], 1), reverse=True)

    # Skill gaps that appeared 2+ times
    recurring_gaps = [
        {"capability": name, "requests": count}
        for name, count in skill_gaps.most_common(10)
        if count >= 2
    ]

    result = {
        "timestamp": time.time(),
        "days_analyzed": days,
        "sessions_analyzed": len(sessions),
        "total_tool_calls": total_calls_parsed,
        "total_messages": len(all_messages),
        "total_errors": total_errors,
        "overall_success_rate": overall_success,
        "weakest_tools": weakest_tools[:15],
        "user_corrections": sum(corrections_by_type.values()),
        "corrections_by_type": dict(corrections_by_type),
        "correction_samples": correction_samples,
        "skill_gaps": recurring_gaps,
        "retry_patterns": retries[:10],
        "sessions": [
            {
                "id": s["id"],
                "source": s["source"],
                "model": s["model"],
                "tool_calls": s["tool_call_count"],
                "messages": s["message_count"],
                "started_at": s["started_at"],
            }
            for s in sessions[:20]
        ],
    }

    return result


def _detect_retry_patterns(messages: list[dict]) -> list[dict]:
    """Detect retry loops: same tool called 3+ times within 30 seconds."""
    retries = []
    prev_tool = None
    prev_time = 0
    prev_session = None
    consecutive_count = 0

    for msg in messages:
        if msg["role"] == "assistant" and msg.get("tool_calls"):
            try:
                calls = json.loads(msg["tool_calls"]) if isinstance(msg["tool_calls"], str) else msg["tool_calls"]
                for call in (calls if isinstance(calls, list) else [calls]):
                    tool_name = call.get("name") or call.get("function", {}).get("name", "")
                    ts = msg["timestamp"]
                    if tool_name == prev_tool and (ts - prev_time) < 30:
                        consecutive_count += 1
                    else:
                        if consecutive_count >= 3:  # Min 4 calls to flag as loop
                            retries.append({
                                "tool": prev_tool,
                                "count": consecutive_count + 1,
                                "session_id": prev_session,
                            })
                        consecutive_count = 0
                    prev_tool = tool_name
                    prev_time = ts
                    prev_session = msg["session_id"]
            except (json.JSONDecodeError, TypeError, AttributeError):
                continue

    if consecutive_count >= 3:
        retries.append({
            "tool": prev_tool,
            "count": consecutive_count + 1,
            "session_id": prev_session,
        })

    return retries


def print_dashboard(data: dict):
    """Print a human-readable dashboard."""
    if "error" in data:
        print(f"Error: {data['error']}")
        return

    if data["sessions_analyzed"] == 0:
        print(data.get("message", "No sessions found."))
        return

    print("=" * 60)
    print("  HERMES DOJO — REFINED ANALYSIS")
    print("=" * 60)
    print(f"  Sessions:     {data['sessions_analyzed']} (last {data['days_analyzed']} days)")
    print(f"  Messages:     {data['total_messages']}")
    print(f"  Tool calls:   {data['total_tool_calls']}")
    print(f"  Overall:      {data['overall_success_rate']}% success ({data['total_errors']} errors)")
    print()

    if data["weakest_tools"]:
        print("  TOP WEAKNESSES:")
        print("  " + "-" * 56)
        for i, t in enumerate(data["weakest_tools"][:7], 1):
            marker = " ⚠️" if t["success_rate"] < 70 else ""
            print(f"  {i}. {t['tool']:<25} {t['success_rate']:>7}% ({t['errors']}/{t['total']}){marker}")
            if t["top_error"]:
                err = t["top_error"][:70]
                print(f"     → {err}")
        print()

    # Show strong tools (all tools with > 90% and >= 3 calls)
    strong = [t for t in data["weakest_tools"] if t["success_rate"] >= 90 and t["total"] >= 3]
    if strong:
        print(f"  STRONG TOOLS (>90%):")
        for t in strong[:5]:
            print(f"     • {t['tool']}: {t['success_rate']}% ({t['total']} calls)")
        print()

    if data["user_corrections"] > 0:
        print(f"  CORRECTIONS: {data['user_corrections']}")
        for ctype, count in data.get("corrections_by_type", {}).items():
            print(f"     • '{ctype}': {count}x")
        print()

    if data["skill_gaps"]:
        print("  SKILL GAPS (by keyword, may be false positives):")
        for gap in data["skill_gaps"]:
            print(f"     • {gap['capability']}: requested {gap['requests']}x")
        print()

    if data["retry_patterns"]:
        print(f"  RETRY PATTERNS: {len(data['retry_patterns'])}")
        for r in data["retry_patterns"][:3]:
            print(f"     • {r['tool']}: {r['count']}x calls")
        print()

    # Legend for weakness markers
    print("  ⚠️  = tool with < 70% success rate")
    print()
    print("  NOTE: monitor.py counts 0 tool calls due to state.db tool_name=NULL bug.")
    print("  This refined analyzer builds tool names from assistant tool_calls JSON.")
    print("=" * 60)


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="Hermes Dojo Refined Analyzer")
    parser.add_argument("--days", type=int, default=7, help="Days to analyze (default: 7)")
    parser.add_argument("--json", action="store_true", help="Output as JSON")
    parser.add_argument("--session-id", type=str, help="Analyze specific session")
    parser.add_argument("--save-snapshot", action="store_true", help="Save to tracker metrics")
    args = parser.parse_args()

    data = analyze_sessions(days=args.days, session_id=args.session_id)

    if args.save_snapshot:
        try:
            sys.path.insert(0, str(Path(__file__).parent))
            from tracker import save_snapshot
            snap = save_snapshot(data)
            if args.json:
                print(json.dumps({"analysis": data, "snapshot": snap}, indent=2, default=str))
            else:
                print_dashboard(data)
                print(f"\nSnapshot saved: {snap['date']} — {snap['overall_success_rate']}%")
        except Exception as e:
            print(f"Warning: couldn't save snapshot: {e}", file=sys.stderr)
            if args.json:
                print(json.dumps(data, indent=2, default=str))
            else:
                print_dashboard(data)
    elif args.json:
        print(json.dumps(data, indent=2, default=str))
    else:
        print_dashboard(data)
