# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# SPDX-License-Identifier: MIT

import asyncio
import inspect
import logging
from typing import Any, Callable, List, Optional

from langchain.agents import create_agent as langchain_create_agent
from langchain.agents.middleware import AgentMiddleware
from langgraph.runtime import Runtime

from src.agents.tool_interceptor import wrap_tools_with_interceptor
from src.config.agents import AGENT_LLM_MAP
from src.llms.llm import get_llm_by_type
from src.prompts import apply_prompt_template

logger = logging.getLogger(__name__)


class DynamicPromptMiddleware(AgentMiddleware):
    """Middleware to apply dynamic prompt template before model invocation.
    
    This middleware prepends a system message with the rendered prompt template
    to the messages list before the model is called.
    """
    
    def __init__(self, prompt_template: str, locale: str = "en-US"):
        self.prompt_template = prompt_template
        self.locale = locale
    
    def before_model(self, state: Any, runtime: Runtime) -> dict[str, Any] | None:
        """Apply prompt template and prepend system message to messages."""
        try:
            # Get the rendered messages including system prompt from template
            rendered_messages = apply_prompt_template(
                self.prompt_template, state, locale=self.locale
            )
            # The first message is the system prompt, extract it
            if rendered_messages and len(rendered_messages) > 0:
                system_message = rendered_messages[0]
                # Prepend system message to existing messages
                return {"messages": [system_message]}
            return None
        except Exception as e:
            logger.error(
                f"Failed to apply prompt template in before_model: {e}",
                exc_info=True
            )
            return None

    async def abefore_model(self, state: Any, runtime: Runtime) -> dict[str, Any] | None:
        """Async version of before_model."""
        return self.before_model(state, runtime)


class PreModelHookMiddleware(AgentMiddleware):
    """Middleware to execute a pre-model hook before model invocation.
    
    This middleware wraps the legacy pre_model_hook callable and executes it
    as part of the middleware chain.
    """
    
    def __init__(self, pre_model_hook: Callable):
        self._pre_model_hook = pre_model_hook
    
    def before_model(self, state: Any, runtime: Runtime) -> dict[str, Any] | None:
        """Execute the pre-model hook."""
        if not self._pre_model_hook:
            return None
        
        try:
            result = self._pre_model_hook(state, runtime)
            return result
        except Exception as e:
            logger.error(
                f"Pre-model hook execution failed in before_model: {e}",
                exc_info=True
            )
            return None

    async def abefore_model(self, state: Any, runtime: Runtime) -> dict[str, Any] | None:
        """Async version of before_model."""
        if not self._pre_model_hook:
            return None
        
        try:
            # Check if the hook is async
            if inspect.iscoroutinefunction(self._pre_model_hook):
                result = await self._pre_model_hook(state, runtime)
            else:
                # Run synchronous hook in thread pool to avoid blocking event loop
                result = await asyncio.to_thread(self._pre_model_hook, state, runtime)
            return result
        except Exception as e:
            logger.error(
                f"Pre-model hook execution failed in abefore_model: {e}",
                exc_info=True
            )
            return None


# Create agents using configured LLM types
def create_agent(
    agent_name: str,
    agent_type: str,
    tools: list,
    prompt_template: str,
    pre_model_hook: callable = None,
    interrupt_before_tools: Optional[List[str]] = None,
    locale: str = "en-US",
):
    """Factory function to create agents with consistent configuration.

    Args:
        agent_name: Name of the agent
        agent_type: Type of agent (researcher, coder, etc.)
        tools: List of tools available to the agent
        prompt_template: Name of the prompt template to use
        pre_model_hook: Optional hook to preprocess state before model invocation
        interrupt_before_tools: Optional list of tool names to interrupt before execution
        locale: Language locale for prompt template selection (e.g., en-US, zh-CN)

    Returns:
        A configured agent graph
    """
    logger.debug(
        f"Creating agent '{agent_name}' of type '{agent_type}' "
        f"with {len(tools)} tools and template '{prompt_template}'"
    )
    
    # Wrap tools with interrupt logic if specified
    processed_tools = tools
    if interrupt_before_tools:
        logger.info(
            f"Creating agent '{agent_name}' with tool-specific interrupts: {interrupt_before_tools}"
        )
        logger.debug(f"Wrapping {len(tools)} tools for agent '{agent_name}'")
        processed_tools = wrap_tools_with_interceptor(tools, interrupt_before_tools)
        logger.debug(f"Agent '{agent_name}' tool wrapping completed")
    else:
        logger.debug(f"Agent '{agent_name}' has no interrupt-before-tools configured")

    if agent_type not in AGENT_LLM_MAP:
        logger.warning(
            f"Agent type '{agent_type}' not found in AGENT_LLM_MAP. "
            f"Falling back to default LLM type 'basic' for agent '{agent_name}'. "
            "This may indicate a configuration issue."
        )
    llm_type = AGENT_LLM_MAP.get(agent_type, "basic")
    logger.debug(f"Agent '{agent_name}' using LLM type: {llm_type}")
    
    logger.debug(f"Creating agent '{agent_name}' with locale: {locale}")
    
    # Build middleware list
    # Use closure to capture locale from the workflow state instead of relying on
    # agent state.get("locale"), which doesn't have the locale field
    # See: https://github.com/bytedance/deer-flow/issues/743
    middleware = [DynamicPromptMiddleware(prompt_template, locale)]
    
    # Add pre-model hook middleware if provided
    if pre_model_hook:
        middleware.append(PreModelHookMiddleware(pre_model_hook))
    
    agent = langchain_create_agent(
        name=agent_name,
        model=get_llm_by_type(llm_type),
        tools=processed_tools,
        middleware=middleware,
    )
    logger.info(f"Agent '{agent_name}' created successfully")
    
    return agent
