"""Obol hook handler for Cursor editor.

Receives tool use events via stdin JSON from Cursor hooks,
sends them to Obol API as audit events. Zero external dependencies.

Cursor hooks format: JSON in, JSON out with permission/continue fields.
Config: ~/.cursor/hooks.json

Usage in Cursor hooks config:
    {
      "hooks": {
        "afterShellExecution": [{"command": "python3 ~/.obol/cursor_handler.py"}],
        "afterFileEdit": [{"command": "python3 ~/.obol/cursor_handler.py"}],
        "afterMCPExecution": [{"command": "python3 ~/.obol/cursor_handler.py"}]
      }
    }
"""
import datetime
import hashlib
import json
import os
import re
import sys
import urllib.request
import urllib.error


def _load_config():
    """Load API key and agent_id from ~/.obol/config.json."""
    config_path = os.path.expanduser("~/.obol/config.json")
    try:
        with open(config_path) as f:
            return json.load(f)
    except Exception:
        return {}


def _scrub_secrets(text):
    """Redact sensitive patterns from text."""
    if not text:
        return text
    text = re.sub(r'Bearer\s+\S+', 'Bearer [REDACTED]', text)
    text = re.sub(r'ob_(live|test)_[a-f0-9]+', r'ob_\1_[REDACTED]', text)
    text = re.sub(
        r'(api[_-]?key|token|secret|password|authorization)\s*[=:]\s*\S+',
        r'\1=[REDACTED]',
        text,
        flags=re.IGNORECASE,
    )
    return text


def _content_hash(data):
    """Hash content for audit trail without storing content."""
    if not data:
        return None
    raw = json.dumps(data, sort_keys=True).encode()
    return hashlib.sha256(raw).hexdigest()[:16]


def _detect_obfuscation(command):
    """Check a command for obfuscation patterns."""
    if not command:
        return []
    patterns = []
    if re.search(r'\bbase64\s+(-d|--decode)', command):
        patterns.append("base64")
    if re.search(r'(?:^|[;&|])\s*eval\s', command, re.MULTILINE):
        patterns.append("eval")
    m = re.search(r'[A-Za-z0-9+/]{80,}={0,2}', command)
    if m and re.search(r'[a-z]', m.group()) and re.search(r'[A-Z]', m.group()) and re.search(r'[0-9]', m.group()):
        patterns.append("encoded_string")
    if len(re.findall(r'\$(?![(\s])', command)) >= 6:
        patterns.append("variable_expansion")
    return patterns


def _map_cursor_event(data):
    """Map Cursor hook event to Obol event format."""
    hook_event = data.get("hook_event_name", "")

    # Cursor event types
    if hook_event == "afterShellExecution":
        command = data.get("command", "")
        return {
            "action": "Bash",
            "target": _scrub_secrets(command[:2000]) if command else None,
            "result": "success" if data.get("exitCode", 0) == 0 else "error",
            "obfuscation": _detect_obfuscation(command),
            "metadata": {
                "exit_code": data.get("exitCode"),
                "stdout_size": len(data.get("stdout", "")),
                "stderr_size": len(data.get("stderr", "")),
            }
        }

    elif hook_event == "beforeShellExecution":
        command = data.get("command", "")
        return {
            "action": "Bash.pre",
            "target": _scrub_secrets(command[:2000]) if command else None,
            "result": "pending",
            "obfuscation": _detect_obfuscation(command),
        }

    elif hook_event == "afterFileEdit":
        file_path = data.get("filePath", "")
        return {
            "action": "Edit",
            "target": file_path,
            "result": "success",
            "metadata": {
                "lines_changed": data.get("linesChanged"),
            }
        }

    elif hook_event == "afterMCPExecution":
        tool_name = data.get("toolName", "mcp_tool")
        tool_input = data.get("toolInput", {})
        return {
            "action": f"MCP.{tool_name}",
            "target": _content_hash(tool_input),
            "result": "success" if data.get("success", True) else "error",
            "metadata": {
                "mcp_server": data.get("serverName"),
                "mcp_tool": tool_name,
            }
        }

    elif hook_event == "beforeMCPExecution":
        tool_name = data.get("toolName", "mcp_tool")
        return {
            "action": f"MCP.{tool_name}.pre",
            "target": _content_hash(data.get("toolInput", {})),
            "result": "pending",
        }

    elif hook_event == "beforeReadFile":
        return {
            "action": "Read.pre",
            "target": data.get("filePath", ""),
            "result": "pending",
        }

    elif hook_event in ("afterAgentResponse", "afterAgentThought"):
        return {
            "action": "agent.response",
            "target": None,
            "result": "success",
            "metadata": {
                "model": data.get("model"),
            }
        }

    elif hook_event == "stop":
        return {
            "action": "session.end",
            "target": None,
            "result": "success",
        }

    else:
        # Generic fallback
        return {
            "action": hook_event or "unknown",
            "target": None,
            "result": "success",
        }


def main():
    # Read event from stdin (Cursor sends JSON)
    try:
        data = json.load(sys.stdin)
    except Exception:
        # Return success JSON so Cursor continues
        print(json.dumps({"continue": True}))
        sys.exit(0)

    config = _load_config()

    api_key = os.environ.get("OBOL_API_KEY") or config.get("api_key", "")
    api_url = os.environ.get("OBOL_API_URL") or config.get("api_url", "https://obol.grayowlresearch.com/v1")
    agent_id = config.get("cursor_agent_id")

    if not api_key or not agent_id:
        # Not configured - return success so Cursor continues
        print(json.dumps({"continue": True}))
        sys.exit(0)

    # Map Cursor event to Obol format
    mapped = _map_cursor_event(data)

    session_id = data.get("conversationId") or data.get("generationId") or "cursor-session"

    event = {
        "agent_id": agent_id,
        "session_id": session_id[:200],
        "action": mapped["action"][:100],
        "target": mapped.get("target"),
        "target_hash": _content_hash(data),
        "result": mapped["result"],
        "detected_patterns": mapped.get("obfuscation", []),
        "metadata": {
            "platform": "cursor",
            "hook_event": data.get("hook_event_name", ""),
            "cursor_version": data.get("cursorVersion", ""),
            **(mapped.get("metadata") or {}),
        },
    }

    # Send to Obol API
    body = json.dumps(event).encode()
    req = urllib.request.Request(
        f"{api_url.rstrip('/')}/events",
        data=body,
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
            "User-Agent": "obol-sdk/0.1.0",
        },
        method="POST",
    )

    try:
        urllib.request.urlopen(req, timeout=10)
    except Exception:
        # Log to local fallback
        try:
            failed_path = os.path.expanduser("~/.obol/failed_events.jsonl")
            event["attempted_at"] = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None).isoformat() + "Z"
            with open(failed_path, "a") as f:
                f.write(json.dumps(event) + "\n")
        except Exception:
            pass

    # Return success JSON so Cursor continues
    print(json.dumps({"continue": True}))
    sys.exit(0)


if __name__ == "__main__":
    main()
