"""Obol interceptors - monkey-patch Python to monitor agent I/O.

Two types of interceptors:
1. Builtins: Patches open(), subprocess, os.* for SDK session monitoring
2. Frameworks: Patches LangChain, CrewAI, AutoGen, OpenAI SDK for tool call capture

Usage:
    # For SDK session monitoring (builtins)
    from obol.interceptors import install_interceptors, uninstall_interceptors
    install_interceptors(callback)

    # For AI framework monitoring
    import obol
    obol.auto_instrument()  # Patches all detected frameworks

    # Or selectively:
    from obol.interceptors import langchain, crewai, autogen, openai_sdk
    langchain.install()
"""
import builtins
import hashlib
import os
import subprocess
import sys
import time
import importlib.util
from typing import Callable, Optional

# --- Builtins interceptor state ---
_originals = {}
_callback = None
_builtins_installed = False

# --- Framework interceptor state ---
_frameworks_installed = set()


def install_interceptors(callback: Callable):
    """Install builtins interceptors. Callback receives (action, target, content, result, duration_ms)."""
    global _callback, _builtins_installed
    if _builtins_installed:
        return

    _callback = callback
    _builtins_installed = True

    # File I/O
    _originals["open"] = builtins.open
    builtins.open = _wrapped_open

    # OS operations
    _originals["os.system"] = os.system
    os.system = _wrapped_os_system

    _originals["os.remove"] = os.remove
    os.remove = _wrapped_os_remove

    _originals["os.rename"] = os.rename
    os.rename = _wrapped_os_rename

    _originals["os.makedirs"] = os.makedirs
    os.makedirs = _wrapped_os_makedirs

    # Subprocess
    _originals["subprocess.run"] = subprocess.run
    subprocess.run = _wrapped_subprocess_run

    _originals["subprocess.Popen"] = subprocess.Popen
    subprocess.Popen = _wrapped_subprocess_popen

    # Import hook for network libraries
    _install_import_hooks()


def uninstall_interceptors():
    """Restore all original builtins functions."""
    global _callback, _builtins_installed
    if not _builtins_installed:
        return

    builtins.open = _originals.get("open", builtins.open)
    os.system = _originals.get("os.system", os.system)
    os.remove = _originals.get("os.remove", os.remove)
    os.rename = _originals.get("os.rename", os.rename)
    os.makedirs = _originals.get("os.makedirs", os.makedirs)
    subprocess.run = _originals.get("subprocess.run", subprocess.run)
    subprocess.Popen = _originals.get("subprocess.Popen", subprocess.Popen)

    _uninstall_import_hooks()

    _callback = None
    _builtins_installed = False
    _originals.clear()


def _emit(action: str, target: str, content: bytes = None,
          result: str = "success", duration_ms: int = None):
    """Send event to callback (non-blocking)."""
    if _callback:
        try:
            _callback(action, target, content, result, duration_ms)
        except Exception:
            pass  # Never let monitoring break the agent


# --- File I/O interceptors ---

def _wrapped_open(*args, **kwargs):
    """Intercept file open operations."""
    start = time.monotonic_ns()
    filepath = str(args[0]) if args else kwargs.get("file", "unknown")
    mode = args[1] if len(args) > 1 else kwargs.get("mode", "r")

    try:
        result = _originals["open"](*args, **kwargs)
        duration = (time.monotonic_ns() - start) // 1_000_000

        action = "file_write" if any(c in mode for c in "wax+") else "file_read"

        # For reads, try to capture content hash
        content = None
        if "r" in mode and "b" not in mode:
            try:
                pos = result.tell()
                content = result.read().encode()
                result.seek(pos)
            except Exception:
                pass
        elif "r" in mode:
            try:
                pos = result.tell()
                content = result.read()
                result.seek(pos)
            except Exception:
                pass

        _emit(action, filepath, content, "success", duration)
        return result

    except Exception as e:
        duration = (time.monotonic_ns() - start) // 1_000_000
        _emit("file_open", filepath, None, "error", duration)
        raise


# --- OS interceptors ---

def _wrapped_os_system(command):
    start = time.monotonic_ns()
    try:
        ret = _originals["os.system"](command)
        duration = (time.monotonic_ns() - start) // 1_000_000
        result = "success" if ret == 0 else "error"
        _emit("os_system", command, None, result, duration)
        return ret
    except Exception as e:
        duration = (time.monotonic_ns() - start) // 1_000_000
        _emit("os_system", command, None, "error", duration)
        raise


def _wrapped_os_remove(path, *args, **kwargs):
    start = time.monotonic_ns()
    try:
        _originals["os.remove"](path, *args, **kwargs)
        duration = (time.monotonic_ns() - start) // 1_000_000
        _emit("file_delete", str(path), None, "success", duration)
    except Exception as e:
        duration = (time.monotonic_ns() - start) // 1_000_000
        _emit("file_delete", str(path), None, "error", duration)
        raise


def _wrapped_os_rename(src, dst, *args, **kwargs):
    start = time.monotonic_ns()
    try:
        _originals["os.rename"](src, dst, *args, **kwargs)
        duration = (time.monotonic_ns() - start) // 1_000_000
        _emit("file_rename", f"{src} -> {dst}", None, "success", duration)
    except Exception as e:
        duration = (time.monotonic_ns() - start) // 1_000_000
        _emit("file_rename", f"{src} -> {dst}", None, "error", duration)
        raise


def _wrapped_os_makedirs(name, *args, **kwargs):
    start = time.monotonic_ns()
    try:
        _originals["os.makedirs"](name, *args, **kwargs)
        duration = (time.monotonic_ns() - start) // 1_000_000
        _emit("dir_create", str(name), None, "success", duration)
    except Exception as e:
        duration = (time.monotonic_ns() - start) // 1_000_000
        _emit("dir_create", str(name), None, "error", duration)
        raise


# --- Subprocess interceptors ---

def _wrapped_subprocess_run(*args, **kwargs):
    start = time.monotonic_ns()
    cmd = args[0] if args else kwargs.get("args", "unknown")
    cmd_str = " ".join(cmd) if isinstance(cmd, (list, tuple)) else str(cmd)

    try:
        result = _originals["subprocess.run"](*args, **kwargs)
        duration = (time.monotonic_ns() - start) // 1_000_000
        status = "success" if result.returncode == 0 else "error"
        _emit("subprocess", cmd_str, None, status, duration)
        return result
    except Exception as e:
        duration = (time.monotonic_ns() - start) // 1_000_000
        _emit("subprocess", cmd_str, None, "error", duration)
        raise


class _WrappedPopen:
    """Wrapper around subprocess.Popen that records the creation."""

    def __init__(self, *args, **kwargs):
        cmd = args[0] if args else kwargs.get("args", "unknown")
        cmd_str = " ".join(cmd) if isinstance(cmd, (list, tuple)) else str(cmd)
        _emit("subprocess_popen", cmd_str, None, "success", 0)
        self._real = _originals["subprocess.Popen"](*args, **kwargs)

    def __getattr__(self, name):
        return getattr(self._real, name)

    def __enter__(self):
        self._real.__enter__()
        return self

    def __exit__(self, *args):
        return self._real.__exit__(*args)


def _wrapped_subprocess_popen(*args, **kwargs):
    return _WrappedPopen(*args, **kwargs)


# --- Import hooks for network libraries ---

_import_hook_installed = False
_original_import = None


def _install_import_hooks():
    global _import_hook_installed, _original_import
    if _import_hook_installed:
        return

    _original_import = builtins.__import__
    _import_hook_installed = True

    def _hooked_import(name, *args, **kwargs):
        module = _original_import(name, *args, **kwargs)

        # Patch requests library on first import
        if name == "requests" and hasattr(module, "Session"):
            _patch_requests(module)

        # Detect potential escape attempts
        if name in ("ctypes", "cffi"):
            _emit("escape_attempt", f"import {name}", None, "success", 0)

        return module

    builtins.__import__ = _hooked_import


def _uninstall_import_hooks():
    global _import_hook_installed, _original_import
    if _import_hook_installed and _original_import:
        builtins.__import__ = _original_import
        _import_hook_installed = False


def _patch_requests(requests_module):
    """Patch requests.Session to monitor HTTP calls."""
    if hasattr(requests_module.Session, "_obol_patched"):
        return

    original_request = requests_module.Session.request

    def monitored_request(self, method, url, **kwargs):
        start = time.monotonic_ns()
        try:
            resp = original_request(self, method, url, **kwargs)
            duration = (time.monotonic_ns() - start) // 1_000_000
            content = resp.content if resp.content else None
            _emit("http_request", f"{method} {url}", content, "success", duration)
            return resp
        except Exception as e:
            duration = (time.monotonic_ns() - start) // 1_000_000
            _emit("http_request", f"{method} {url}", None, "error", duration)
            raise

    requests_module.Session.request = monitored_request
    requests_module.Session._obol_patched = True


# ============================================================
# AI Framework Interceptors (LangChain, CrewAI, AutoGen, etc.)
# ============================================================


def _is_package_installed(package):
    """Check if a Python package is available."""
    try:
        return importlib.util.find_spec(package) is not None
    except Exception:
        return False


def auto_instrument():
    """Detect and instrument all available AI frameworks.

    Safe to call multiple times - only instruments each framework once.
    """
    instrumented = []

    # LangChain
    if _is_package_installed("langchain_core") and "langchain" not in _frameworks_installed:
        try:
            from obol.interceptors import langchain
            langchain.install()
            _frameworks_installed.add("langchain")
            instrumented.append("langchain")
        except Exception:
            pass

    # CrewAI (also uses LangChain tools)
    if _is_package_installed("crewai") and "crewai" not in _frameworks_installed:
        try:
            from obol.interceptors import crewai
            crewai.install()
            _frameworks_installed.add("crewai")
            instrumented.append("crewai")
        except Exception:
            pass

    # AutoGen
    if (_is_package_installed("autogen") or _is_package_installed("pyautogen")) and "autogen" not in _frameworks_installed:
        try:
            from obol.interceptors import autogen
            autogen.install()
            _frameworks_installed.add("autogen")
            instrumented.append("autogen")
        except Exception:
            pass

    # OpenAI SDK
    if _is_package_installed("openai") and "openai" not in _frameworks_installed:
        try:
            from obol.interceptors import openai_sdk
            openai_sdk.install()
            _frameworks_installed.add("openai")
            instrumented.append("openai")
        except Exception:
            pass

    return instrumented


def get_installed():
    """Return set of currently instrumented frameworks."""
    return _frameworks_installed.copy()


def is_instrumented(framework):
    """Check if a specific framework is instrumented."""
    return framework in _frameworks_installed


# Environment variable auto-instrumentation
if os.environ.get("OBOL_AUTO_INSTRUMENT", "").lower() in ("1", "true", "yes"):
    auto_instrument()
