"""Digital Employee Agent class.

This module provides the DigitalEmployeeAgent class that encapsulates
agent creation, configuration, and deployment logic separate from the
main DigitalEmployee orchestrator.

Authors:
    Immanuel Rhesa (immanuel.rhesa@gdplabs.id)

References:
    NONE
"""

from dataclasses import dataclass, field
from typing import Any, Callable

from glaip_sdk import MCP, Agent, Tool
from glaip_sdk.agents.component import AgentComponent
from gllm_core.utils import LoggerManager
from omegaconf import OmegaConf

from digital_employee_core.config_templates.loader import DEFAULTS_TEMPLATE, ConfigTemplateLoader
from digital_employee_core.configuration.agent_configuration import AgentConfigKeys, MemoryProvider, StepLimitConfig
from digital_employee_core.constants import DEFAULT_MODEL_NAME
from digital_employee_core.constants.models import ensure_valid_model_name
from digital_employee_core.digital_employee.config import DigitalEmployeeConfig
from digital_employee_core.digital_employee.config_propagation import ConfigPropagationManager, ItemType
from digital_employee_core.digital_employee.dependencies import (
    DependencyBundle,
    MCPConfigBuilder,
    ToolConfigBuilder,
)
from digital_employee_core.escalation import EscalationConfig
from digital_employee_core.escalation.base_escalation_channel import EscalationChannel
from digital_employee_core.escalation.constant import (
    ESCALATION_NO_CHANNELS_DEPENDENCIES_WARNING,
    ESCALATION_NO_CHANNELS_PROMPT_WARNING,
    ESCALATION_PROMPT,
)
from digital_employee_core.identity.identity import (
    DigitalEmployeeIdentity,
)
from digital_employee_core.schedule import ScheduleItemConfig

logger = LoggerManager().get_logger(__name__)


_VALID_MEMORY_PROVIDERS = frozenset(provider.value for provider in MemoryProvider)


@dataclass
class DigitalEmployeeAgent:
    """Digital Employee Agent manager.

    This class handles agent creation, configuration, and deployment.
    It manages tools, MCPs, escalation channels, and agent deployment.

    Attributes:
        identity (DigitalEmployeeIdentity): The digital employee's identity.
        tools (list[Tool]): List of tools the agent can use.
        sub_agents (list[Agent]): List of sub-agents for delegation.
        mcps (list[MCP]): List of MCPs the agent can use.
        configurations (list[DigitalEmployeeConfig]): List of configuration objects.
        model (str): Model identifier to use for the agent.
        agent_config (dict[str, Any] | Agent._UNSET): Agent execution configuration.
        agent_coordinator_name (str | None): Optional name for agent coordinator.
        schedules (list[ScheduleItemConfig]): List of schedule configurations for the agent.
        escalation_config (EscalationConfig): Escalation configuration.
        agent (Agent | None): The deployed agent instance.
    """

    identity: DigitalEmployeeIdentity
    tools: list[Tool] = field(default_factory=list)
    sub_agents: list[Agent] = field(default_factory=list)
    mcps: list[MCP] = field(default_factory=list)
    configurations: list[DigitalEmployeeConfig] = field(default_factory=list)
    model: str = DEFAULT_MODEL_NAME
    agent_config: dict[str, Any] | None = None
    agent_coordinator_name: str | None = None
    schedules: list[ScheduleItemConfig] = field(default_factory=list)
    escalation_config: EscalationConfig = field(default_factory=lambda: EscalationConfig(enabled=False, channels=[]))
    agent: Agent | None = field(default=None, init=False)
    config_loaders: list[ConfigTemplateLoader] = field(default_factory=list, init=False)

    def __post_init__(self) -> None:
        """Initialize derived fields after dataclass initialization.

        Performs validation and normalization:
        - Coerces None to empty lists for list fields (backward compatibility)
        - Validates and normalizes model name
        - Validates agent_config or sets to Agent._UNSET
        - Strips whitespace from agent_coordinator_name
        - Initializes config_loaders with base loader if empty
        """
        # Coerce None to empty lists for backward compatibility with subclasses
        # that may pass None explicitly (e.g., examples/subclass_example.py)
        if self.tools is None:
            self.tools = []
        if self.sub_agents is None:
            self.sub_agents = []
        if self.mcps is None:
            self.mcps = []
        if self.configurations is None:
            self.configurations = []
        if self.schedules is None:
            self.schedules = []
        if self.escalation_config is None:
            self.escalation_config = EscalationConfig(enabled=False, channels=[])

        self.model = ensure_valid_model_name(self.model)
        # TODO: make agent config strong typing
        self.agent_config = (
            self._ensure_valid_agent_config(self.agent_config) if self.agent_config is not None else Agent._UNSET
        )
        if self.agent_coordinator_name is not None:
            self.agent_coordinator_name = self.agent_coordinator_name.strip() or None
        # Initialize with base config loader - subclasses can add more loaders
        if not self.config_loaders:
            self.config_loaders = [ConfigTemplateLoader()]

    @property
    def _is_escalation_active(self) -> bool:
        """Check if escalation is currently active.

        Returns:
            bool: True if escalation is enabled, a supervisor is set, and channels are configured.
        """
        return bool(self.escalation_config.enabled and self.identity.supervisor and self.escalation_config.channels)

    def add_tools(self, tools: list[Tool]) -> None:
        """Add tools to the agent.

        Args:
            tools (list[Tool]): List of tools to add.
        """
        self.tools.extend(tools)

    def remove_tools(self, tools: list[Tool]) -> None:
        """Remove tools from the agent.

        Args:
            tools (list[Tool]): List of tools to remove.
        """
        tools_to_remove = set(tools)
        self.tools = [tool for tool in self.tools if tool not in tools_to_remove]

    def add_mcps(self, mcps: list[MCP]) -> None:
        """Add MCPs to the agent.

        Args:
            mcps (list[MCP]): List of MCPs to add.
        """
        self.mcps.extend(mcps)

    def remove_mcps(self, mcps: list[MCP]) -> None:
        """Remove MCPs from the agent.

        Args:
            mcps (list[MCP]): List of MCPs to remove.
        """
        mcps_to_remove = set(mcps)
        self.mcps = [mcp for mcp in self.mcps if mcp not in mcps_to_remove]

    def add_config_loader(self, config_loader: ConfigTemplateLoader) -> None:
        """Add a config loader to the list of loaders.

        Args:
            config_loader (ConfigTemplateLoader): Config loader to add.
        """
        self.config_loaders.append(config_loader)

    def add_sub_agents(self, sub_agents: list[Agent]) -> None:
        """Add sub-agents to the agent.

        Args:
            sub_agents (list[Agent]): List of sub-agents to add.
        """
        self.sub_agents.extend(sub_agents)

    def remove_sub_agents(self, sub_agents: list[Agent]) -> None:
        """Remove sub-agents by name.

        Args:
            sub_agents (list[Agent]): List of sub-agents to remove.
        """
        agent_names_to_remove = {agent.name for agent in sub_agents if agent.name}
        self.sub_agents = [agent for agent in self.sub_agents if agent.name not in agent_names_to_remove]

    def enable_escalation(self) -> None:
        """Enable escalation for the agent."""
        self.escalation_config.enabled = True

    def disable_escalation(self) -> None:
        """Disable escalation for the agent."""
        self.escalation_config.enabled = False

    def add_escalation_channel(self, channel: EscalationChannel) -> None:
        """Add an escalation channel to the agent.

        Args:
            channel (EscalationChannel): The escalation channel to add.
        """
        if channel not in self.escalation_config.channels:
            self.escalation_config.channels.append(channel)

    def remove_escalation_channel(self, channel: EscalationChannel) -> None:
        """Remove an escalation channel from the agent.

        Args:
            channel (EscalationChannel): The escalation channel to remove.
        """
        if channel in self.escalation_config.channels:
            self.escalation_config.channels.remove(channel)

    def add_schedule(self, schedules: list[ScheduleItemConfig] | ScheduleItemConfig) -> "DigitalEmployeeAgent":
        """Add schedule configuration(s) to the digital employee.

        This method supports the builder pattern, allowing chaining of add_schedule calls.

        Args:
            schedules (list[ScheduleItemConfig] | ScheduleItemConfig): The schedule configuration(s) to add.
                Can be a single ScheduleItemConfig or a list of ScheduleItemConfig objects.

        Returns:
            DigitalEmployeeAgent: Returns self to support builder pattern.
        """
        # Convert single schedule to list for uniform handling
        if isinstance(schedules, ScheduleItemConfig):
            self.schedules.append(schedules)
        else:
            self.schedules.extend(schedules)
        return self

    def get_schedule(self) -> list[ScheduleItemConfig]:
        """Get the current schedule configurations.

        Returns:
            list[ScheduleItemConfig]: The list of current schedule configurations.
        """
        return self.schedules

    def clear_schedule(self) -> None:
        """Clear all schedule configurations from the digital employee."""
        self.schedules = []

    def remove_schedule(self, schedule: ScheduleItemConfig) -> None:
        """Remove a specific schedule configuration from the digital employee.

        Args:
            schedule (ScheduleItemConfig): The schedule configuration to remove.
        """
        if schedule in self.schedules:
            self.schedules.remove(schedule)

    def build_prompt(self) -> str:
        """Build the prompt for the agent.

        This method can be overridden by subclasses to provide custom
        prompts based on the digital employee's identity and job.

        Returns:
            str: The prompt string for the agent.

        Raises:
            ConfigurationValidationError: If required placeholders in instruction cannot be resolved.
        """
        language_names = [lang.value for lang in self.identity.languages]

        if len(language_names) > 1:
            language_list = ", ".join(language_names[:-1]) + f" and {language_names[-1]}"
            language_instruction = (
                f"You must respond only in the following languages: {language_list}. "
                f"Choose the most appropriate language based on the user's input or context."
            )
        else:
            language_instruction = f"You must respond only in {language_names[0]} language."

        identity_prefix = (
            f"You are {self.identity.name} ({self.identity.email}), {self.identity.job.title}. {language_instruction}"
        )

        # Resolve placeholders in job instruction using all config loaders
        # Use the first loader as the base, and merge additional loaders' defaults
        base_loader = self.config_loaders[0] if self.config_loaders else ConfigTemplateLoader()
        additional_loaders = self.config_loaders[1:] if len(self.config_loaders) > 1 else None

        # TODO: Change Agent prompt should not use from job instruction
        resolved_instruction = base_loader.resolve_placeholders(
            self.identity.job.instruction,
            self.configurations,
            additional_loaders=additional_loaders,
        )

        prompt = f"{identity_prefix}\n\n{resolved_instruction}"

        if not (self.escalation_config.enabled and self.identity.supervisor):
            return prompt

        if not self.escalation_config.channels:
            logger.warning(ESCALATION_NO_CHANNELS_PROMPT_WARNING)
            return prompt

        if self._is_escalation_active:
            return self._inject_escalation_prompt(prompt)

        return prompt

    def deploy(self) -> Agent:
        """Deploy the agent.

        This method initializes the agent with tools and MCPs,
        builds configurations, and deploys it to the AI platform.

        Returns:
            Agent: The deployed agent instance.
        """
        self.agent = self._build_agent_instance()
        self.agent.deploy()

        if self.schedules:
            # Delete existing schedules before creating new ones
            existing_schedules = self.agent.schedule.list()
            for existing_schedule in existing_schedules:
                logger.info(f"Deleting existing schedule: {existing_schedule.id}")
                self.agent.schedule.delete(existing_schedule.id)

            # Create all new schedules
            for schedule_item in self.schedules:
                self.agent.schedule.create(input=schedule_item.input, schedule=schedule_item.schedule_config)
        return self.agent

    def get_agent_component(self) -> AgentComponent:
        """Get an AgentComponent for pipeline integration.

        Returns:
            AgentComponent: A component wrapping the agent for pipeline use.

        Raises:
            RuntimeError: If agent has not been deployed.
        """
        if self.agent is None:
            self.agent = self._build_agent_instance()
        return self.agent.to_component()

    def build_tool_config(
        self, configurations: list[DigitalEmployeeConfig], tools: list[Tool] | None = None
    ) -> dict[str, dict[str, Any]]:
        """Build tool configuration based on identity and configurations.

        This method loads tool configuration templates from YAML and replaces
        placeholders with values from DigitalEmployeeConfig objects.

        Args:
            configurations (list[DigitalEmployeeConfig]): List of configuration objects.
            tools (list[Tool] | None): Optional list of tools to build configs for.
                If None, uses self.tools.

        Returns:
            dict[str, dict[str, Any]]: Dictionary mapping tool names to their configuration dictionaries.
        """
        if tools is None:
            tools = self.tools
        return self._build_merged_configs(
            configurations, lambda loader, configs, items: loader.load_tool_configs(configs, items), tools
        )

    def build_mcp_config(
        self, configurations: list[DigitalEmployeeConfig], mcps: list[MCP] | None = None
    ) -> dict[str, dict[str, Any]]:
        """Build MCP configuration based on identity and configurations.

        This method loads MCP configuration templates from YAML and replaces
        placeholders with values from DigitalEmployeeConfig objects.

        Args:
            configurations (list[DigitalEmployeeConfig]): List of configuration objects.
            mcps (list[MCP] | None): Optional list of MCPs to build configs for.
                If None, uses self.mcps.

        Returns:
            dict[str, dict[str, Any]]: Dictionary mapping MCP names to their configuration dictionaries.
        """
        if mcps is None:
            mcps = self.mcps
        return self._build_merged_configs(
            configurations, lambda loader, configs, items: loader.load_mcp_configs(configs, items), mcps
        )

    def _process_sub_agents_with_propagation(self) -> list[Agent]:
        """Process sub-agents to propagate configurations from parent.

        For each Agent sub-agent:
        - Check if it has configs for all tools/MCPs it uses
        - For tools/MCPs without configs, propagate from parent if available
        - Merge existing configs with propagated configs (existing takes precedence)
        - This will recursively process sub-agents of sub-agents as well

        Returns:
            list[Agent]: Processed sub-agents with propagated configurations.
        """
        # Use ConfigPropagationManager to collect dependencies and process sub-agents
        propagation_manager = ConfigPropagationManager()

        # Collect only tools and MCPs from sub-agents that DON'T already have configs
        # This prevents trying to build configs for tools that already have them
        tools_needing_configs = propagation_manager.collect_dependencies_needing_configs(
            sub_agents=self.sub_agents, dependency_type=ItemType.TOOL
        )
        mcps_needing_configs = propagation_manager.collect_dependencies_needing_configs(
            sub_agents=self.sub_agents, dependency_type=ItemType.MCP
        )

        # Build parent's configurations for parent's own dependencies AND sub-agent dependencies that need configs
        parent_tool_configs = self.build_tool_config(self.configurations) if self.configurations else {}
        parent_mcp_configs = self.build_mcp_config(self.configurations) if self.configurations else {}

        # Also build configs for tools used by sub-agents that don't have configs yet
        if self.configurations and tools_needing_configs:
            # Merge parent's own tools with sub-agents' tools that need configs
            all_tools = list(set(self.tools + tools_needing_configs))
            parent_tool_configs = self.build_tool_config(self.configurations, all_tools)

        # Also build configs for MCPs used by sub-agents that don't have configs yet
        if self.configurations and mcps_needing_configs:
            # Merge parent's own MCPs with sub-agents' MCPs that need configs
            all_mcps = list(set(self.mcps + mcps_needing_configs))
            parent_mcp_configs = self.build_mcp_config(self.configurations, all_mcps)

        # Process sub-agents with propagation
        return propagation_manager.process_sub_agents_with_propagation(
            sub_agents=self.sub_agents,
            parent_tool_configs=parent_tool_configs,
            parent_mcp_configs=parent_mcp_configs,
        )

    def _build_agent_instance(self) -> Agent:
        """Build and return a new Agent instance with current configuration.

        Returns:
            Agent: A new Agent instance configured with tools, MCPs, and settings.
        """
        tools, mcps, tool_configs, mcp_configs = self._prepare_tools_mcps_and_configs()

        processed_sub_agents = self._process_sub_agents_with_propagation()

        return Agent(
            name=self.agent_coordinator_name or self.identity.name,
            description=self.identity.job.description,
            instruction=self.build_prompt(),
            tools=tools,
            agents=processed_sub_agents,
            mcps=mcps,
            tool_configs=tool_configs,
            mcp_configs=mcp_configs,
            model=self.model,
            agent_config=self.agent_config,
        )

    def _build_merged_configs(
        self,
        configurations: list[DigitalEmployeeConfig],
        load_fn: Callable[
            [ConfigTemplateLoader, list[DigitalEmployeeConfig], list[Any] | None], dict[str, dict[str, Any]]
        ],
        filter_items: list[Any] | None = None,
    ) -> dict[str, dict[str, Any]]:
        """Build merged configurations from all config loaders.

        This is a helper method that reduces code duplication between
        build_tool_config() and build_mcp_config().

        Args:
            configurations (list[DigitalEmployeeConfig]): List of configuration objects.
            load_fn (Callable): Function that takes (loader, configurations, filter_items) and returns configs.
            filter_items (list[Any] | None, optional): Items to filter (tools or MCPs). Defaults to None.

        Returns:
            dict[str, dict[str, Any]]: Dictionary mapping names to their configuration dictionaries.
        """
        if not self.config_loaders:
            return {}

        # First, merge all defaults from all loaders
        merged_defaults = OmegaConf.create({})
        for loader in self.config_loaders:
            defaults_path = loader.template_dir / DEFAULTS_TEMPLATE
            if defaults_path.exists():
                loader_defaults = OmegaConf.load(defaults_path)
                merged_defaults = OmegaConf.merge(merged_defaults, loader_defaults)

        # Convert merged defaults to DigitalEmployeeConfig list
        defaults_dict = OmegaConf.to_container(merged_defaults, resolve=False)
        defaults_config_list = [DigitalEmployeeConfig(key=str(k), value=v) for k, v in defaults_dict.items()]

        # Merge: defaults + user configurations (user takes precedence)
        all_configs = defaults_config_list + configurations

        # Start with configs from the first loader (base) - using merged configs
        result = load_fn(self.config_loaders[0], all_configs, filter_items)

        # Merge configs from additional loaders (if any) - using merged configs
        for loader in self.config_loaders[1:]:
            additional_configs = load_fn(loader, all_configs, filter_items)
            result = ConfigTemplateLoader.merge_configs(result, additional_configs)

        return result

    def _build_tools_and_mcps_configs(
        self,
        tools: list[Tool],
        mcps: list[MCP],
        build_tool_configs: ToolConfigBuilder,
        build_mcp_configs: MCPConfigBuilder,
    ) -> tuple[dict[str, dict[str, Any]] | None, dict[str, dict[str, Any]] | None]:
        """Build tool and MCP configuration maps for the given dependencies.

        This helper builds configuration dictionaries only when instance
        configurations are present (`self.configurations`).

        Args:
            tools (list[Tool]): Tools to build configurations for.
            mcps (list[MCP]): MCPs to build configurations for.
            build_tool_configs (ToolConfigBuilder): Callable that builds tool configs.
            build_mcp_configs (MCPConfigBuilder): Callable that builds MCP configs.

        Returns:
            tuple[dict[str, dict[str, Any]] | None, dict[str, dict[str, Any]] | None]:
                Tool config map and MCP config map.
        """
        tool_configs = None
        mcp_configs = None
        if self.configurations:
            tool_configs = build_tool_configs(self.configurations, tools)
            mcp_configs = build_mcp_configs(self.configurations, mcps)

        return tool_configs, mcp_configs

    def _build_base_tools_mcps_and_configs(
        self,
    ) -> tuple[list[Tool], list[MCP], dict[str, dict[str, Any]] | None, dict[str, dict[str, Any]] | None]:
        """Build base Tools/MCPs and their configurations.

        Returns:
            tuple[list[Tool], list[MCP], dict[str, dict[str, Any]] | None, dict[str, dict[str, Any]] | None]:
                Base tools, base MCPs, tool config map, and MCP config map.
        """
        tool_configs, mcp_configs = self._build_tools_and_mcps_configs(
            tools=self.tools,
            mcps=self.mcps,
            build_tool_configs=lambda configurations, _tools: self.build_tool_config(configurations),
            build_mcp_configs=lambda configurations, _mcps: self.build_mcp_config(configurations),
        )
        return self.tools, self.mcps, tool_configs, mcp_configs

    def _build_escalation_tools_mcps_and_configs(
        self,
    ) -> tuple[list[Tool], list[MCP], dict[str, dict[str, Any]] | None, dict[str, dict[str, Any]] | None]:
        """Build escalation Tools/MCPs and their configurations.

        Returns:
            tuple[list[Tool], list[MCP], dict[str, dict[str, Any]] | None, dict[str, dict[str, Any]] | None]:
                Escalation tools, escalation MCPs, tool config map, and MCP config map.
        """
        tools, mcps = self._build_escalation_dependencies()
        loader = self.config_loaders[0] if self.config_loaders else ConfigTemplateLoader()
        tool_configs, mcp_configs = self._build_tools_and_mcps_configs(
            tools=tools,
            mcps=mcps,
            build_tool_configs=loader.load_tool_configs,
            build_mcp_configs=loader.load_mcp_configs,
        )
        return tools, mcps, tool_configs, mcp_configs

    def _build_base_dependency_bundle(self) -> DependencyBundle:
        """Build the base dependency bundle containing tools, MCPs, and their configurations.

        Returns:
            DependencyBundle: Bundle containing base tools, MCPs, and their config maps.
        """
        tools, mcps, tool_configs, mcp_configs = self._build_base_tools_mcps_and_configs()
        return DependencyBundle(tools=tools, mcps=mcps, tool_configs=tool_configs, mcp_configs=mcp_configs)

    def _build_escalation_dependency_bundle(self) -> DependencyBundle:
        """Build the escalation dependency bundle containing tools, MCPs, and their configurations.

        Returns:
            DependencyBundle: Bundle containing escalation tools, MCPs, and their config maps.
        """
        tools, mcps, tool_configs, mcp_configs = self._build_escalation_tools_mcps_and_configs()
        return DependencyBundle(tools=tools, mcps=mcps, tool_configs=tool_configs, mcp_configs=mcp_configs)

    def _prepare_tools_mcps_and_configs(
        self,
    ) -> tuple[list[Tool], list[MCP], dict[str, dict[str, Any]] | None, dict[str, dict[str, Any]] | None]:
        """Prepare the final Tools/MCPs lists and their configurations.

        This combines base dependencies with escalation dependencies and merges their configurations.

        Returns:
            tuple[list[Tool], list[MCP], dict[str, dict[str, Any]] | None, dict[str, dict[str, Any]] | None]:
                Tools, MCPs, tool config map, and MCP config map to be used for agent deployment.
        """
        base = self._build_base_dependency_bundle()
        escalation = self._build_escalation_dependency_bundle()
        merged = base.merged_with(
            escalation,
            merge_configs=bool(self.configurations),
        )
        return merged.tools, merged.mcps, merged.tool_configs, merged.mcp_configs

    def _build_escalation_prompt(self) -> str:
        """Build the escalation protocol section of the prompt.

        Returns:
            str: The escalation protocol instructions.
        """
        if not self.identity.supervisor:
            return ""

        escalation_prompt_lines = [ESCALATION_PROMPT]

        for channel in self.escalation_config.channels:
            escalation_prompt_lines.append(channel.get_prompt_instruction(self.identity.supervisor))

        return "\n".join(escalation_prompt_lines).strip()

    def _inject_escalation_prompt(self, prompt: str) -> str:
        """Inject the escalation protocol section into the prompt.

        Args:
            prompt (str): The original prompt.

        Returns:
            str: The prompt with the escalation protocol section injected.
        """
        new_prompt = f"{prompt}\n\n{self._build_escalation_prompt()}"
        return new_prompt

    def _build_escalation_dependencies(self) -> tuple[list[Tool], list[MCP]]:
        """Build tools and MCPs required by escalation channels.

        Returns:
            tuple[list[Tool], list[MCP]]: Tuple containing the required tools and MCPs.
        """
        if not (self.escalation_config.enabled and self.identity.supervisor):
            return [], []

        if not self.escalation_config.channels:
            logger.warning(ESCALATION_NO_CHANNELS_DEPENDENCIES_WARNING)
            return [], []

        required_mcps = DependencyBundle.dedupe_preserve_order(
            [mcp for channel in self.escalation_config.channels for mcp in channel.get_required_mcps()]
        )
        required_tools = DependencyBundle.dedupe_preserve_order(
            [tool for channel in self.escalation_config.channels for tool in channel.get_required_tools()]
        )

        return required_tools, required_mcps

    def _normalize_step_limit_config(self, step_limit_config: Any) -> dict[str, Any]:
        """Normalize step limit configuration to a dictionary.

        Args:
            step_limit_config (Any): Step limit configuration, either a StepLimitConfig instance or dict.

        Returns:
            dict[str, Any]: Normalized step limit configuration as a dictionary.

        Raises:
            TypeError: If step_limit_config is not a StepLimitConfig instance or dict.
        """
        if isinstance(step_limit_config, dict):
            step_limit_object = StepLimitConfig.model_validate(step_limit_config)
        elif isinstance(step_limit_config, StepLimitConfig):
            step_limit_object = step_limit_config
        else:
            raise TypeError(
                f"step_limit_config must be a StepLimitConfig instance or dict, got {type(step_limit_config).__name__}"
            )

        return step_limit_object.to_dict()

    def _ensure_valid_agent_config(self, agent_config: dict[str, Any]) -> dict[str, Any]:
        """Ensure agent config is valid.

        Args:
            agent_config (dict[str, Any]): Agent config to validate.

        Returns:
            dict[str, Any]: Validated agent config.
        """
        agent_config = agent_config.copy()
        if AgentConfigKeys.STEP_LIMIT_CONFIG in agent_config:
            step_limit_dict = self._normalize_step_limit_config(agent_config[AgentConfigKeys.STEP_LIMIT_CONFIG])
            if step_limit_dict:
                agent_config[AgentConfigKeys.STEP_LIMIT_CONFIG] = step_limit_dict
            else:
                agent_config.pop(AgentConfigKeys.STEP_LIMIT_CONFIG, None)

        if AgentConfigKeys.MEMORY in agent_config:
            agent_config[AgentConfigKeys.MEMORY] = self._normalize_memory_provider(agent_config[AgentConfigKeys.MEMORY])

        return agent_config

    def _normalize_memory_provider(self, memory: Any) -> str | None:
        """Normalize memory provider to a string value.

        Args:
            memory (Any): Memory provider, either a MemoryProvider enum, string, or None.

        Returns:
            str | None: Normalized memory provider string, or None if not provided.

        Raises:
            TypeError: If memory is not a MemoryProvider, str, or None.
            ValueError: If memory provider string is not a supported provider.
        """
        if memory is None:
            memory_value = None
        elif isinstance(memory, MemoryProvider):
            memory_value = memory.value
        elif isinstance(memory, str):
            memory_value = memory.strip().lower()
        else:
            raise TypeError(f"memory must be a str or MemoryProvider, got {type(memory).__name__}")

        if memory_value is not None and memory_value not in _VALID_MEMORY_PROVIDERS:
            raise ValueError(f"Unknown memory provider: {memory_value}. Supported: {list(_VALID_MEMORY_PROVIDERS)}")

        return memory_value
