"""
LangGraph auto-instrumentation.

Automatically patches LangGraph workflows to create traces and inject handlers.
"""

import functools
import logging
from datetime import datetime
from typing import Any, Dict, Set

from .error_detection import get_error_detector

logger = logging.getLogger(__name__)


async def _handle_invoke_error_async(
    aigie,
    trace,
    error: Exception,
    workflow_name: str,
) -> None:
    """Update trace to failed status on error (async version)."""
    if not trace or not aigie:
        return

    error_detector = get_error_detector()
    detected_error = error_detector.detect_from_exception(error, f"graph:{workflow_name}", {})

    from ...buffer import EventType

    update_data = {
        "id": trace.id if hasattr(trace, "id") else str(trace),
        "status": "failed",
        "error": str(error),
        "error_message": str(error),
        "end_time": datetime.now().isoformat(),
    }

    if detected_error:
        update_data["metadata"] = {
            "error_detection": {
                "type": detected_error.error_type.value,
                "severity": detected_error.severity.value,
                "is_transient": detected_error.is_transient,
            }
        }

    if aigie._buffer:
        await aigie._buffer.add(EventType.TRACE_UPDATE, update_data)


def _handle_invoke_error_sync(
    aigie,
    trace,
    error: Exception,
    workflow_name: str,
) -> None:
    """Update trace to failed status on error (sync version)."""
    if not trace or not aigie:
        return

    error_detector = get_error_detector()
    detected_error = error_detector.detect_from_exception(error, f"graph:{workflow_name}", {})

    from ...buffer import EventType

    update_data = {
        "id": trace.id if hasattr(trace, "id") else str(trace),
        "status": "failed",
        "error": str(error),
        "error_message": str(error),
        "end_time": datetime.now().isoformat(),
    }

    if detected_error:
        update_data["metadata"] = {
            "error_detection": {
                "type": detected_error.error_type.value,
                "severity": detected_error.severity.value,
                "is_transient": detected_error.is_transient,
            }
        }

    if aigie._buffer:
        import asyncio

        try:
            loop = asyncio.get_event_loop()
            if loop.is_running():
                # Schedule the coroutine and track the future so events aren't lost
                future = asyncio.ensure_future(
                    aigie._buffer.add(EventType.TRACE_UPDATE, update_data)
                )
                # Add error handler to avoid unhandled exception warnings
                future.add_done_callback(
                    lambda f: f.exception() if f.done() and not f.cancelled() else None
                )
            else:
                loop.run_until_complete(aigie._buffer.add(EventType.TRACE_UPDATE, update_data))
        except RuntimeError:
            # No event loop available, try to create one
            try:
                asyncio.run(aigie._buffer.add(EventType.TRACE_UPDATE, update_data))
            except Exception:
                logger.debug("Could not send error update to buffer")


_patched_classes: Set[Any] = set()


def _safe_add_callback(config: dict, handler) -> None:
    """Safely add a callback handler to config, regardless of callback type.

    Handles plain lists, AsyncCallbackManager, CallbackManager, and other types.
    """
    existing = config.get("callbacks")
    if existing is None:
        config["callbacks"] = [handler]
    elif isinstance(existing, list):
        existing.append(handler)
    else:
        # AsyncCallbackManager or CallbackManager — extract handlers and convert to list
        handlers = list(getattr(existing, "handlers", []))
        handlers += list(getattr(existing, "inheritable_handlers", []))
        config["callbacks"] = handlers + [handler]


def patch_langgraph() -> bool:
    """Patch LangGraph classes for auto-instrumentation.

    Returns:
        True if patching was successful (or already patched)
    """
    success = True
    success = _patch_state_graph() and success
    # NOTE: _patch_compiled_graph() removed — it caused infinite recursion when
    # combined with _patch_state_graph() compile-time patching and langchain's
    # Runnable.ainvoke patch. The compile-time approach is sufficient.
    success = _patch_tool_node() and success
    return success


def unpatch_langgraph() -> None:
    """Remove LangGraph patches (for testing)."""
    global _patched_classes
    _patched_classes.clear()


def is_langgraph_patched() -> bool:
    """Check if LangGraph has been patched."""
    return len(_patched_classes) > 0


def _extract_workflow_name(inputs: Any, graph_schema: Any = None) -> str:
    """Extract meaningful workflow name from inputs or graph schema.

    Priority:
    1. inputs.metadata.workflow_name
    2. inputs.metadata.workflow_type
    3. inputs.metadata.use_case
    4. Graph schema class name (e.g., ResearchState -> research_workflow)
    5. Fallback to "LangGraph Workflow"
    """
    if isinstance(inputs, dict):
        metadata = inputs.get("metadata", {})
        if isinstance(metadata, dict):
            # Check for explicit workflow name
            if metadata.get("workflow_name"):
                return metadata["workflow_name"]
            # Check for workflow type
            if metadata.get("workflow_type"):
                wf_type = metadata["workflow_type"]
                # Convert to readable name: "deep_research" -> "deep_research_workflow"
                if not wf_type.endswith("_workflow"):
                    return f"{wf_type}_workflow"
                return wf_type
            # Check for use case
            if metadata.get("use_case"):
                return f"{metadata['use_case']}_workflow"

    # Try to extract from graph schema class name
    if graph_schema is not None:
        schema_name = None
        if hasattr(graph_schema, "__name__"):
            schema_name = graph_schema.__name__
        elif hasattr(graph_schema, "__class__"):
            schema_name = graph_schema.__class__.__name__

        if schema_name and schema_name not in ("dict", "Dict", "TypedDict", "State"):
            # Convert PascalCase to snake_case: ResearchState -> research_state
            import re

            snake_name = re.sub(r"(?<!^)(?=[A-Z])", "_", schema_name).lower()
            # Remove 'state' suffix if present: research_state -> research
            if snake_name.endswith("_state"):
                snake_name = snake_name[:-6]
            if snake_name:
                return f"{snake_name}_workflow"

    return "LangGraph Workflow"


def _patch_state_graph() -> bool:
    """Patch StateGraph.compile() to return auto-instrumented app."""
    try:
        from langgraph.graph import StateGraph

        if StateGraph in _patched_classes:
            return True
        if getattr(StateGraph.compile, "_aigie_patched", False):
            return True  # Already patched by primary

        original_compile = StateGraph.compile

        @functools.wraps(original_compile)
        def traced_compile(self, **kwargs):
            """Traced version of compile."""
            app = original_compile(self, **kwargs)

            # Store reference to graph schema for name extraction
            graph_schema = None
            if hasattr(self, "schema") and self.schema:
                graph_schema = self.schema
            elif hasattr(self, "channels") and self.channels:
                # Try to get schema from first channel's annotation
                graph_schema = next(iter(self.channels.keys()), None) if self.channels else None

            # Patch the compiled app's invoke methods
            if hasattr(app, "ainvoke") and not getattr(app.ainvoke, "_aigie_patched", False):
                original_ainvoke = app.ainvoke

                async def traced_ainvoke(
                    inputs: Any, config: Dict[str, Any] | None = None, **kwargs
                ):
                    from ...auto_instrument.trace import get_or_create_trace
                    from ...callback import AigieCallbackHandler
                    from ...client import get_aigie
                    from ...langgraph import LangGraphHandler

                    aigie = get_aigie()
                    trace = None
                    workflow_name = "LangGraph Workflow"

                    if aigie and aigie._initialized:
                        # Extract meaningful workflow name
                        workflow_name = _extract_workflow_name(inputs, graph_schema)

                        trace = await get_or_create_trace(
                            name=workflow_name,
                            metadata={
                                "type": "langgraph",
                                "inputs": inputs if isinstance(inputs, dict) else {},
                                "framework": "langgraph",
                            },
                        )

                        # Ensure _current_trace is set for nested LLM calls
                        from ...auto_instrument.trace import set_current_trace

                        set_current_trace(trace)

                        # Create LangGraph handler for node tracking
                        langgraph_handler = LangGraphHandler(
                            trace_name=workflow_name,
                            metadata={"type": "langgraph", "framework": "langgraph"},
                        )
                        langgraph_handler._aigie = aigie
                        langgraph_handler._trace_context = trace
                        langgraph_handler.trace_id = trace.id if trace else None

                        # Initialize remediation engine for recommendation-mode reporting
                        from .config import LangGraphConfig

                        lg_config = LangGraphConfig.from_env()
                        if lg_config.enable_realtime_remediation:
                            api_url = getattr(aigie, "_api_url", None) or getattr(
                                aigie, "api_url", None
                            )
                            api_key = getattr(aigie, "_api_key", None) or getattr(
                                aigie, "api_key", None
                            )
                            if api_url:
                                from ...realtime.remediation_engine import RemediationEngine

                                langgraph_handler._remediation_engine = RemediationEngine(
                                    api_url=api_url,
                                    api_key=api_key or "",
                                    query_timeout=lg_config.remediation_query_timeout,
                                )

                        # Create LangChain callback handler for LLM/tool tracking
                        # Link to LangGraphHandler so it can access current node span
                        callback_handler = AigieCallbackHandler(trace=trace)
                        callback_handler._langgraph_handler = langgraph_handler

                        if config is None:
                            config = {}
                        _safe_add_callback(config, langgraph_handler)
                        _safe_add_callback(config, callback_handler)

                        # Set run_name in metadata for LangChain tracing
                        if "metadata" not in config:
                            config["metadata"] = {}
                        config["metadata"]["run_name"] = workflow_name

                    # Subscribe to gateway push interventions
                    _sg_dispatcher = (
                        getattr(aigie, "_intervention_dispatcher", None) if aigie else None
                    )
                    _sg_trace_id = trace.id if trace and hasattr(trace, "id") else None
                    if _sg_dispatcher and _sg_trace_id:
                        _sg_dispatcher.subscribe_trace(_sg_trace_id)

                    try:
                        return await original_ainvoke(inputs, config=config, **kwargs)
                    except Exception as e:
                        # Update trace to failed status
                        await _handle_invoke_error_async(aigie, trace, e, workflow_name)
                        raise
                    finally:
                        if _sg_dispatcher and _sg_trace_id:
                            _sg_dispatcher.unsubscribe_trace(_sg_trace_id)

                traced_ainvoke._aigie_patched = True
                app.ainvoke = traced_ainvoke

            if hasattr(app, "invoke") and not getattr(app.invoke, "_aigie_patched", False):
                original_invoke = app.invoke

                def traced_invoke(inputs: Any, config: Dict[str, Any] | None = None, **kwargs):
                    """Traced version of invoke."""
                    from ...auto_instrument.trace import get_or_create_trace_sync
                    from ...callback import AigieCallbackHandler
                    from ...client import get_aigie
                    from ...langgraph import LangGraphHandler

                    aigie = get_aigie()
                    trace = None
                    workflow_name = "LangGraph Workflow"

                    if aigie and aigie._initialized:
                        # Extract meaningful workflow name
                        workflow_name = _extract_workflow_name(inputs, graph_schema)

                        trace = get_or_create_trace_sync(
                            name=workflow_name,
                            metadata={
                                "type": "langgraph",
                                "inputs": inputs if isinstance(inputs, dict) else {},
                                "framework": "langgraph",
                            },
                        )

                        if trace:
                            # Ensure _current_trace is set for nested LLM calls
                            from ...auto_instrument.trace import set_current_trace

                            set_current_trace(trace)

                            # Create LangGraph handler for node tracking
                            langgraph_handler = LangGraphHandler(
                                trace_name=workflow_name,
                                metadata={"type": "langgraph", "framework": "langgraph"},
                            )
                            langgraph_handler._aigie = aigie
                            langgraph_handler._trace_context = trace
                            langgraph_handler.trace_id = trace.id if trace else None

                            # Initialize remediation engine for recommendation-mode reporting
                            from .config import LangGraphConfig

                            lg_config = LangGraphConfig.from_env()
                            if lg_config.enable_realtime_remediation:
                                api_url = getattr(aigie, "_api_url", None) or getattr(
                                    aigie, "api_url", None
                                )
                                api_key = getattr(aigie, "_api_key", None) or getattr(
                                    aigie, "api_key", None
                                )
                                if api_url:
                                    from ...realtime.remediation_engine import RemediationEngine

                                    langgraph_handler._remediation_engine = RemediationEngine(
                                        api_url=api_url,
                                        api_key=api_key or "",
                                        query_timeout=lg_config.remediation_query_timeout,
                                    )

                            # Create LangChain callback handler for LLM/tool tracking
                            # Link to LangGraphHandler so it can access current node span
                            callback_handler = AigieCallbackHandler(trace=trace)
                            callback_handler._langgraph_handler = langgraph_handler

                            if config is None:
                                config = {}
                            _safe_add_callback(config, langgraph_handler)
                            _safe_add_callback(config, callback_handler)

                            # Set run_name in metadata for LangChain tracing
                            if "metadata" not in config:
                                config["metadata"] = {}
                            config["metadata"]["run_name"] = workflow_name

                    # Subscribe to gateway push interventions
                    _sg_dispatcher = (
                        getattr(aigie, "_intervention_dispatcher", None) if aigie else None
                    )
                    _sg_trace_id = trace.id if trace and hasattr(trace, "id") else None
                    if _sg_dispatcher and _sg_trace_id:
                        _sg_dispatcher.subscribe_trace(_sg_trace_id)

                    try:
                        return original_invoke(inputs, config=config, **kwargs)
                    except Exception as e:
                        # Update trace to failed status
                        _handle_invoke_error_sync(aigie, trace, e, workflow_name)
                        raise
                    finally:
                        if _sg_dispatcher and _sg_trace_id:
                            _sg_dispatcher.unsubscribe_trace(_sg_trace_id)

                traced_invoke._aigie_patched = True
                app.invoke = traced_invoke

            return app

        traced_compile._aigie_patched = True
        StateGraph.compile = traced_compile
        _patched_classes.add(StateGraph)

        logger.debug("Patched StateGraph.compile for auto-instrumentation")
        return True

    except ImportError:
        logger.debug("LangGraph not installed, skipping StateGraph patch")
        return True  # Not an error if LangGraph not installed
    except Exception as e:
        logger.warning(f"Failed to patch StateGraph: {e}")
        return False


def _patch_compiled_graph() -> bool:
    """Patch CompiledStateGraph/Pregel methods directly."""
    try:
        # Try to import the actual compiled graph class (varies by langgraph version)
        CompiledGraph = None
        try:
            from langgraph.graph.state import CompiledStateGraph

            CompiledGraph = CompiledStateGraph
        except ImportError:
            pass

        if not CompiledGraph:
            try:
                from langgraph.graph.graph import CompiledGraph as CG

                CompiledGraph = CG
            except ImportError:
                pass

        if not CompiledGraph:
            try:
                from langgraph.pregel import Pregel

                CompiledGraph = Pregel
            except ImportError:
                pass

        if not CompiledGraph:
            logger.debug("Could not find CompiledGraph/CompiledStateGraph/Pregel class to patch")
            return True

        if CompiledGraph in _patched_classes:
            return True
        if getattr(CompiledGraph.ainvoke, "_aigie_patched", False):
            return True  # Already patched by primary

        logger.debug(f"Patching {CompiledGraph.__name__} for auto-instrumentation")

        original_ainvoke = CompiledGraph.ainvoke
        original_invoke = CompiledGraph.invoke

        @functools.wraps(original_ainvoke)
        async def traced_ainvoke(self, inputs: Any, config: Dict[str, Any] | None = None, **kwargs):
            """Traced version of CompiledGraph.ainvoke."""
            from ...auto_instrument.trace import get_or_create_trace
            from ...callback import AigieCallbackHandler
            from ...client import get_aigie
            from ...langgraph import LangGraphHandler

            aigie = get_aigie()
            trace = None
            workflow_name = "LangGraph Workflow"

            if aigie and aigie._initialized:
                # Extract meaningful workflow name
                graph_schema = getattr(self, "builder", None)
                if graph_schema and hasattr(graph_schema, "schema"):
                    graph_schema = graph_schema.schema
                workflow_name = _extract_workflow_name(inputs, graph_schema)

                trace = await get_or_create_trace(
                    name=workflow_name,
                    metadata={
                        "type": "langgraph",
                        "inputs": inputs if isinstance(inputs, dict) else {},
                        "framework": "langgraph",
                    },
                )

                # Ensure _current_trace is set for nested LLM calls
                from ...auto_instrument.trace import set_current_trace

                set_current_trace(trace)

                # Create LangGraph handler for node tracking
                langgraph_handler = LangGraphHandler(
                    trace_name=workflow_name,
                    metadata={"type": "langgraph", "framework": "langgraph"},
                )
                langgraph_handler._aigie = aigie
                langgraph_handler._trace_context = trace
                langgraph_handler.trace_id = trace.id if trace else None

                # Initialize remediation engine for recommendation-mode reporting
                from .config import LangGraphConfig

                lg_config = LangGraphConfig.from_env()
                if lg_config.enable_realtime_remediation:
                    api_url = getattr(aigie, "_api_url", None) or getattr(aigie, "api_url", None)
                    api_key = getattr(aigie, "_api_key", None) or getattr(aigie, "api_key", None)
                    if api_url:
                        from ...realtime.remediation_engine import RemediationEngine

                        langgraph_handler._remediation_engine = RemediationEngine(
                            api_url=api_url,
                            api_key=api_key or "",
                            query_timeout=lg_config.remediation_query_timeout,
                        )

                # Create LangChain callback handler for LLM/tool tracking
                # Link to LangGraphHandler so it can access current node span
                callback_handler = AigieCallbackHandler(trace=trace)
                callback_handler._langgraph_handler = langgraph_handler

                if config is None:
                    config = {}
                if "callbacks" not in config:
                    config["callbacks"] = []
                config["callbacks"].append(langgraph_handler)
                config["callbacks"].append(callback_handler)

                # Set run_name in metadata for LangChain tracing
                if "metadata" not in config:
                    config["metadata"] = {}
                config["metadata"]["run_name"] = workflow_name

            # Subscribe to gateway push interventions
            _cg_dispatcher = getattr(aigie, "_intervention_dispatcher", None) if aigie else None
            _cg_trace_id = trace.id if trace and hasattr(trace, "id") else None
            if _cg_dispatcher and _cg_trace_id:
                _cg_dispatcher.subscribe_trace(_cg_trace_id)

            try:
                return await original_ainvoke(self, inputs, config=config, **kwargs)
            except Exception as e:
                # Update trace to failed status
                await _handle_invoke_error_async(aigie, trace, e, workflow_name)
                raise
            finally:
                if _cg_dispatcher and _cg_trace_id:
                    _cg_dispatcher.unsubscribe_trace(_cg_trace_id)

        @functools.wraps(original_invoke)
        def traced_invoke(self, inputs: Any, config: Dict[str, Any] | None = None, **kwargs):
            """Traced version of CompiledGraph.invoke."""
            from ...auto_instrument.trace import get_or_create_trace_sync
            from ...callback import AigieCallbackHandler
            from ...client import get_aigie
            from ...langgraph import LangGraphHandler

            aigie = get_aigie()
            trace = None
            workflow_name = "LangGraph Workflow"

            if aigie and aigie._initialized:
                # Extract meaningful workflow name
                graph_schema = getattr(self, "builder", None)
                if graph_schema and hasattr(graph_schema, "schema"):
                    graph_schema = graph_schema.schema
                workflow_name = _extract_workflow_name(inputs, graph_schema)

                trace = get_or_create_trace_sync(
                    name=workflow_name,
                    metadata={
                        "type": "langgraph",
                        "inputs": inputs if isinstance(inputs, dict) else {},
                        "framework": "langgraph",
                    },
                )

                if trace:
                    # Ensure _current_trace is set for nested LLM calls
                    from ...auto_instrument.trace import set_current_trace

                    set_current_trace(trace)

                    # Create LangGraph handler for node tracking
                    langgraph_handler = LangGraphHandler(
                        trace_name=workflow_name,
                        metadata={"type": "langgraph", "framework": "langgraph"},
                    )
                    langgraph_handler._aigie = aigie
                    langgraph_handler._trace_context = trace
                    langgraph_handler.trace_id = trace.id if trace else None

                    # Initialize remediation engine for recommendation-mode reporting
                    from .config import LangGraphConfig

                    lg_config = LangGraphConfig.from_env()
                    if lg_config.enable_realtime_remediation:
                        api_url = getattr(aigie, "_api_url", None) or getattr(
                            aigie, "api_url", None
                        )
                        api_key = getattr(aigie, "_api_key", None) or getattr(
                            aigie, "api_key", None
                        )
                        if api_url:
                            from ...realtime.remediation_engine import RemediationEngine

                            langgraph_handler._remediation_engine = RemediationEngine(
                                api_url=api_url,
                                api_key=api_key or "",
                                query_timeout=lg_config.remediation_query_timeout,
                            )

                    # Create LangChain callback handler for LLM/tool tracking
                    # Link to LangGraphHandler so it can access current node span
                    callback_handler = AigieCallbackHandler(trace=trace)
                    callback_handler._langgraph_handler = langgraph_handler

                    if config is None:
                        config = {}
                    if "callbacks" not in config:
                        config["callbacks"] = []
                    config["callbacks"].append(langgraph_handler)
                    config["callbacks"].append(callback_handler)

                    # Set run_name in metadata for LangChain tracing
                    if "metadata" not in config:
                        config["metadata"] = {}
                    config["metadata"]["run_name"] = workflow_name

            # Subscribe to gateway push interventions
            _cg_dispatcher = getattr(aigie, "_intervention_dispatcher", None) if aigie else None
            _cg_trace_id = trace.id if trace and hasattr(trace, "id") else None
            if _cg_dispatcher and _cg_trace_id:
                _cg_dispatcher.subscribe_trace(_cg_trace_id)

            try:
                return original_invoke(self, inputs, config=config, **kwargs)
            except Exception as e:
                # Update trace to failed status
                _handle_invoke_error_sync(aigie, trace, e, workflow_name)
                raise
            finally:
                if _cg_dispatcher and _cg_trace_id:
                    _cg_dispatcher.unsubscribe_trace(_cg_trace_id)

        traced_ainvoke._aigie_patched = True
        traced_invoke._aigie_patched = True
        CompiledGraph.ainvoke = traced_ainvoke
        CompiledGraph.invoke = traced_invoke
        _patched_classes.add(CompiledGraph)

        logger.debug(f"Patched {CompiledGraph.__name__} for auto-instrumentation")
        return True

    except ImportError:
        return True
    except Exception as e:
        logger.warning(f"Failed to patch CompiledGraph: {e}")
        return False


def _patch_tool_node() -> bool:
    """Patch ToolNode.__init__ to inject remediation wrapper via awrap_tool_call.

    In autonomous mode, wraps the tool call execution so that errors
    (both exceptions and error-status ToolMessages) get remediation
    guidance appended before the model sees them.
    """
    try:
        from langgraph.prebuilt import ToolNode
    except ImportError:
        return True  # LangGraph not installed or ToolNode unavailable

    if ToolNode in _patched_classes:
        return True
    if getattr(ToolNode.__init__, "_aigie_patched", False):
        return True  # Already patched by primary

    original_init = ToolNode.__init__

    @functools.wraps(original_init)
    def traced_init(self, tools, *, awrap_tool_call=None, **kwargs):
        from .config import LangGraphConfig

        config = LangGraphConfig.from_env()

        if config.enable_realtime_remediation and config.remediation_mode == "autonomous":
            from ...client import get_aigie

            aigie = get_aigie()
            if aigie and aigie._initialized:
                api_url = getattr(aigie, "_api_url", None) or getattr(aigie, "api_url", None)
                api_key = getattr(aigie, "_api_key", None) or getattr(aigie, "api_key", None)
                if api_url:
                    import asyncio as _asyncio

                    from ...realtime.remediation_engine import RemediationEngine

                    engine = RemediationEngine(
                        api_url=api_url,
                        api_key=api_key or "",
                        query_timeout=config.remediation_query_timeout,
                    )
                    dispatcher = getattr(aigie, "_intervention_dispatcher", None)
                    user_wrapper = awrap_tool_call

                    async def remediation_wrapper(request, execute):
                        tool_name = (
                            request.tool_call.get("name", "unknown")
                            if isinstance(request.tool_call, dict)
                            else getattr(request.tool_call, "name", "unknown")
                        )
                        tool_call_id = (
                            request.tool_call.get("id", "")
                            if isinstance(request.tool_call, dict)
                            else getattr(request.tool_call, "id", "")
                        )

                        # Check pending gateway intervention before executing
                        if dispatcher:
                            # Try to get trace_id from LangGraph handler in config callbacks
                            trace_id = ""
                            try:
                                from ...langgraph import LangGraphHandler

                                lg_config = getattr(request, "config", None) or {}
                                for cb in lg_config.get("callbacks") or []:
                                    if isinstance(cb, LangGraphHandler) and cb.trace_id:
                                        trace_id = cb.trace_id
                                        break
                            except Exception:
                                pass

                            if trace_id:
                                signal = dispatcher.pop_pending(trace_id)
                                if signal:
                                    if signal.intervention_type == "break_loop":
                                        try:
                                            from langchain_core.messages import ToolMessage

                                            return ToolMessage(
                                                content=f"[Aigie] {signal.reason}",
                                                tool_call_id=tool_call_id,
                                                status="error",
                                            )
                                        except ImportError:
                                            raise RuntimeError(f"[Aigie] {signal.reason}")
                                    elif signal.intervention_type == "delay":
                                        delay_ms = signal.payload.get("delay_ms", 1000)
                                        await _asyncio.sleep(delay_ms / 1000.0)
                                    elif signal.intervention_type == "inject_correction":
                                        corrections = signal.payload.get("corrections", {})
                                        if corrections and isinstance(request.tool_call, dict):
                                            request.tool_call["args"] = {
                                                **request.tool_call.get("args", {}),
                                                **corrections,
                                            }

                        try:
                            result = await (
                                user_wrapper(request, execute) if user_wrapper else execute(request)
                            )
                            # Check for error status in ToolMessage
                            if hasattr(result, "status") and result.status == "error":
                                error_msg = (
                                    result.content
                                    if isinstance(result.content, str)
                                    else str(result.content)
                                )
                                rem = await engine.evaluate(error_msg, tool_name, "", "")
                                if rem:
                                    applied_programmatic = False
                                    # Try structured fix via AutoFixApplicator
                                    if rem.action_type:
                                        try:
                                            fix_action = engine.to_fix_action(rem)
                                            if fix_action:
                                                from ...interceptor.protocols import (
                                                    InterceptionContext,
                                                )
                                                from ...realtime.auto_fix import AutoFixApplicator

                                                applicator = AutoFixApplicator()

                                                async def _retry_fn(**kwargs):
                                                    return await execute(request)

                                                applicator.set_retry_executor(_retry_fn)
                                                ctx = InterceptionContext(
                                                    provider="langgraph",
                                                    model="",
                                                    messages=[],
                                                    response_content=error_msg,
                                                )
                                                fix_result = await applicator.apply_fixes(
                                                    ctx, [fix_action]
                                                )
                                                if fix_result.success:
                                                    applied_programmatic = True
                                                    engine.mark_applied(rem)
                                                    if fix_result.modified_response is not None:
                                                        return fix_result.modified_response
                                        except Exception as fix_err:
                                            logger.debug(
                                                f"[AIGIE] Programmatic fix failed: {fix_err}"
                                            )

                                    # Fall back to text guidance injection
                                    if not applied_programmatic and rem.guidance_text:
                                        engine.mark_applied(rem)
                                        try:
                                            from langchain_core.messages import ToolMessage

                                            return ToolMessage(
                                                content=f"{result.content}\n\n{rem.guidance_text}",
                                                tool_call_id=result.tool_call_id,
                                                status=result.status,
                                            )
                                        except ImportError:
                                            pass
                            return result
                        except Exception as e:
                            rem = await engine.evaluate(str(e), tool_name, "", "")
                            if rem:
                                applied_programmatic = False
                                # Try structured fix via AutoFixApplicator
                                if rem.action_type:
                                    try:
                                        fix_action = engine.to_fix_action(rem)
                                        if fix_action:
                                            from ...interceptor.protocols import InterceptionContext
                                            from ...realtime.auto_fix import AutoFixApplicator

                                            applicator = AutoFixApplicator()

                                            async def _retry_fn_exc(**kwargs):
                                                return await execute(request)

                                            applicator.set_retry_executor(_retry_fn_exc)
                                            ctx = InterceptionContext(
                                                provider="langgraph",
                                                model="",
                                                messages=[],
                                                response_content=str(e),
                                            )
                                            fix_result = await applicator.apply_fixes(
                                                ctx, [fix_action]
                                            )
                                            if fix_result.success:
                                                applied_programmatic = True
                                                engine.mark_applied(rem)
                                                if fix_result.modified_response is not None:
                                                    return fix_result.modified_response
                                    except Exception as fix_err:
                                        logger.debug(f"[AIGIE] Programmatic fix failed: {fix_err}")

                                # Fall back to text guidance injection
                                if not applied_programmatic and rem.guidance_text:
                                    engine.mark_applied(rem)
                                    try:
                                        from langchain_core.messages import ToolMessage

                                        return ToolMessage(
                                            content=f"Error: {e}\n\n{rem.guidance_text}",
                                            tool_call_id=tool_call_id,
                                            status="error",
                                        )
                                    except ImportError:
                                        pass
                            raise

                    awrap_tool_call = remediation_wrapper

        original_init(self, tools, awrap_tool_call=awrap_tool_call, **kwargs)

    traced_init._aigie_patched = True
    ToolNode.__init__ = traced_init
    _patched_classes.add(ToolNode)

    logger.debug("Patched ToolNode.__init__ for remediation auto-instrumentation")
    return True
