#!/usr/bin/env python3
"""
InsAIts -- Claude Code PreToolUse / PostToolUse Hook
====================================================
This script runs as a Claude Code hook to inspect tool inputs (PreToolUse)
or tool outputs (PostToolUse) for anomalies, and injects corrective
feedback into stdout if problems are detected.

Claude Code reads stdout from hooks and surfaces it back to the model.

SETUP (PreToolUse -- recommended, can block dangerous actions):
  Add to your project's .claude/settings.json:
  {
    "hooks": {
      "PreToolUse": [
        {
          "matcher": ".*",
          "hooks": [
            {
              "type": "command",
              "command": "python3 -m insa_its.hooks.claude_code_hook"
            }
          ]
        }
      ]
    }
  }

  Or for PostToolUse (inspect outputs, cannot block):
  {
    "hooks": {
      "PostToolUse": [
        {
          "matcher": "Write|Edit|MultiEdit",
          "hooks": [
            {
              "type": "command",
              "command": "python3 -m insa_its.hooks.claude_code_hook"
            }
          ]
        }
      ]
    }
  }

HOW IT WORKS:
  Claude Code passes tool input/result as JSON on stdin.
  This script inspects it and writes feedback to stdout.
  Claude Code feeds stdout back to the model automatically.
  Exit code 0 = pass through. Exit code 2 = block + show message (PreToolUse only).

Environment variables:
  INSAITS_MODEL      LLM model identifier for fingerprinting. Default: claude-opus.
  INSAITS_FAIL_MODE  "open" (default) = continue on SDK errors.
                     "closed" = block tool execution on SDK errors.
  INSAITS_VERBOSE    Set to any value to enable debug logging.

Author: Cristi Bogdan -- Steddy Nova SRL / YuyAI
"""

from __future__ import annotations

import json
import logging
import os
import sys
from typing import Any, Dict, List, Tuple

# Configure logging to stderr so it does not interfere with stdout protocol
logging.basicConfig(
    stream=sys.stderr,
    format="[InsAIts] %(message)s",
    level=logging.DEBUG if os.environ.get("INSAITS_VERBOSE") else logging.WARNING,
)
log: logging.Logger = logging.getLogger("insaits-hook")

# --- Constants ---
MIN_CONTENT_LENGTH: int = 10
MAX_SCAN_LENGTH: int = 4000
DEFAULT_MODEL: str = "claude-opus"
BLOCKING_SEVERITIES: frozenset = frozenset({"CRITICAL", "HIGH"})

# Import from InsAIts SDK (adapted path)
try:
    from insa_its.ai_monitor import AIMonitor
    AVAILABLE: bool = True
except ImportError:
    AVAILABLE = False


def extract_text_from_tool_result(data: Dict[str, Any]) -> Tuple[str, str]:
    """Extract the response text and task context from a Claude Code tool payload.

    Works for both PreToolUse (tool_input) and PostToolUse (tool_response).

    Returns:
        A (response_text, task_context) tuple.
    """
    tool_name: str = data.get("tool_name", "")
    tool_input: Dict[str, Any] = data.get("tool_input", {})
    tool_result: Any = data.get("tool_response", {})

    response_text: str = ""
    task_context: str = ""

    # For Write/Edit tools -- inspect what was written
    if tool_name in ("Write", "Edit", "MultiEdit"):
        content: str = tool_input.get("content", "") or tool_input.get("new_string", "")
        path: str = tool_input.get("file_path", tool_input.get("path", ""))
        response_text = content
        task_context = f"Write file: {path}"

    # For Bash -- inspect command (PreToolUse) or output (PostToolUse)
    elif tool_name == "Bash":
        command: str = str(tool_input.get("command", ""))
        output: str = ""
        if isinstance(tool_result, dict):
            output = tool_result.get("output", "") or tool_result.get("stdout", "")
        elif isinstance(tool_result, str):
            output = tool_result
        # Prefer tool output if available (PostToolUse), else use command (PreToolUse)
        response_text = output if output else command
        task_context = f"Bash: {command[:80]}"

    # For generic assistant messages
    elif "content" in data:
        raw_content: Any = data["content"]
        if isinstance(raw_content, list):
            texts: List[str] = [
                b.get("text", "") for b in raw_content if b.get("type") == "text"
            ]
            response_text = "\n".join(texts)
        elif isinstance(raw_content, str):
            response_text = raw_content
        task_context = str(data.get("task", ""))

    return response_text, task_context


def format_feedback(issues: List[Dict[str, str]]) -> str:
    """Format InsAIts findings as a clear message for Claude Code.

    Returns:
        A human-readable multi-line string describing each finding.
    """
    lines: List[str] = [
        "== InsAIts Quality Gate -- Issues Detected ==",
        "",
    ]
    for i, issue in enumerate(issues, 1):
        sev: str = issue.get("severity", "MEDIUM")
        itype: str = issue.get("type", "unknown").replace("_", " ").upper()
        desc: str = issue.get("description", "")
        fix: str = issue.get("intervention", "")
        lines.extend([
            f"{i}. [{sev}] {itype}",
            f"   What happened: {desc[:100]}",
            f"   Fix required:  {fix[:120]}",
            "",
        ])
    lines.extend([
        "-" * 56,
        "Address ALL issues above before completing this task.",
        "Call insaits_check tool after your fix to verify.",
    ])
    return "\n".join(lines)


def main() -> None:
    """Main entry point for the Claude Code hook."""
    # Read tool input/result from stdin (Claude Code passes it as JSON)
    raw: str = sys.stdin.read().strip()

    if not raw:
        sys.exit(0)  # Nothing to inspect -- pass through

    try:
        data: Dict[str, Any] = json.loads(raw)
    except json.JSONDecodeError:
        # Not JSON -- might be plain text output, inspect it directly
        data = {"content": raw, "task": ""}

    response_text: str
    task_context: str
    response_text, task_context = extract_text_from_tool_result(data)

    # Skip very short outputs (e.g. "OK", empty bash results)
    if len(response_text.strip()) < MIN_CONTENT_LENGTH:
        sys.exit(0)

    # Skip binary or non-text content
    if not response_text.isprintable() and len(response_text) > 100:
        sys.exit(0)

    if not AVAILABLE:
        # InsAIts not installed -- log and pass through
        log.warning("Monitor not available. Install: pip install insa-its")
        sys.exit(0)

    # Wrap SDK calls so an internal error does not crash the hook
    try:
        monitor: AIMonitor = AIMonitor(
            model_id=os.environ.get("INSAITS_MODEL", DEFAULT_MODEL),
        )
        result = monitor.inspect(
            response_text[:MAX_SCAN_LENGTH],
            task_context,
        )
    except Exception as exc:  # Broad catch intentional: unknown SDK internals
        fail_mode: str = os.environ.get("INSAITS_FAIL_MODE", "open").lower()
        if fail_mode == "closed":
            sys.stdout.write(
                f"InsAIts SDK error ({type(exc).__name__}); "
                "blocking execution to avoid unscanned input.\n"
            )
            sys.exit(2)
        log.warning(
            "SDK error (%s), skipping inspection: %s",
            type(exc).__name__, exc,
        )
        sys.exit(0)

    if result.clean:
        # All good -- pass through silently
        log.debug("Response clean -- all checks passed.")
        sys.exit(0)

    # Issues found -- format feedback and send to Claude Code
    issues: List[Dict[str, str]] = [
        {
            "type": a.anomaly_type.value,
            "severity": a.severity.value,
            "description": a.description,
            "intervention": a.intervention,
        }
        for a in result.anomalies
    ]

    feedback: str = format_feedback(issues)

    # Exit code 2 = Claude Code blocks the action and shows the message
    #               (only effective in PreToolUse)
    # Exit code 0 = pass through (stderr warnings are non-blocking)
    has_critical: bool = any(
        i["severity"] in BLOCKING_SEVERITIES for i in issues
    )

    if has_critical:
        sys.stdout.write(feedback + "\n")
        sys.exit(2)  # block -- Claude must fix before continuing
    else:
        log.warning("\n%s", feedback)
        sys.exit(0)


if __name__ == "__main__":
    main()
