"""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

from glaip_sdk import MCP, Agent, Tool
from glaip_sdk.agents.component import AgentComponent
from glaip_sdk.agents.types import SkillsInput
from gllm_core.utils import LoggerManager

from digital_employee_core.config_templates.loader import ConfigTemplateLoader
from digital_employee_core.configuration.agent_config_validator import AgentConfigValidator
from digital_employee_core.configuration.config_builder import ConfigBuilder
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.escalation import EscalationConfig, EscalationManager
from digital_employee_core.identity.identity import (
    DigitalEmployeeIdentity,
)
from digital_employee_core.schedule import ScheduleItemConfig

logger = LoggerManager().get_logger(__name__)


@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.
        skills (SkillsInput): Skill source(s) for the agent.
        configs (list[DigitalEmployeeConfig]): List of configuration objects.
        model (str): Model identifier to use for the agent.
        agent_config (dict[str, Any] | None): Agent execution configuration.
        agent_coordinator_name (str | None): Optional name for agent coordinator.
        schedules (list[ScheduleItemConfig]): List of schedule configurations for the agent.
        escalation_manager (EscalationManager): Manager for escalation functionality.
            Use this to access and modify escalation settings.
        agent (Agent | None): The deployed agent instance.
        config_loaders (list[ConfigTemplateLoader]): List of configuration template loaders.
        config_builder (ConfigBuilder): Builder for tool and MCP configurations.
    """

    identity: DigitalEmployeeIdentity
    tools: list[Tool] = field(default_factory=list)
    sub_agents: list[Agent] = field(default_factory=list)
    mcps: list[MCP] = field(default_factory=list)
    skills: SkillsInput = field(default_factory=list)
    configs: 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_manager: EscalationManager = field(default=None)
    agent: Agent | None = field(default=None, init=False)
    config_loaders: list[ConfigTemplateLoader] = field(default_factory=list, init=False)
    config_builder: ConfigBuilder | None = field(default=None, 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.configs is None:
            self.configs = []
        if self.schedules is None:
            self.schedules = []

        self.model = ensure_valid_model_name(self.model)
        # TODO: make agent config strong typing
        self.agent_config = (
            AgentConfigValidator.ensure_valid_agent_config(self.agent_config) if self.agent_config is not None else None
        )
        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()]
        # Initialize escalation manager with default config if not provided
        if self.escalation_manager is None:
            default_escalation_config = EscalationConfig(enabled=False, channels=[])
            self.escalation_manager = EscalationManager(default_escalation_config, self.identity.supervisor)
        # Initialize configuration builder
        self.config_builder = ConfigBuilder(
            config_loaders=self.config_loaders,
            configs=self.configs,
            tools=self.tools,
            mcps=self.mcps,
            escalation_manager=self.escalation_manager,
        )

    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_skills(self, skills: SkillsInput) -> None:
        """Add skills to the agent.

        Args:
            skills (SkillsInput): Skill source(s) to add.
        """
        if not skills:
            return

        if not self.skills:
            self.skills = list(skills) if isinstance(skills, list) else [skills]
            return

        current = list(self.skills) if isinstance(self.skills, list) else [self.skills]
        incoming = list(skills) if isinstance(skills, list) else [skills]
        current.extend(incoming)
        self.skills = current

    def remove_skills(self, skills: SkillsInput) -> None:
        """Remove skills from the agent.

        Args:
            skills (SkillsInput): Skill source(s) to remove.
        """
        if not self.skills or not skills:
            return

        current = list(self.skills) if isinstance(self.skills, list) else [self.skills]
        to_remove = list(skills) if isinstance(skills, list) else [skills]
        remaining = [skill for skill in current if skill not in to_remove]

        self.skills = remaining

    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 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.configs,
            additional_loaders=additional_loaders,
        )

        prompt = (
            f"{identity_prefix}\n\n{resolved_instruction}\n\n"
            "IMPORTANT: Preserve any text in the content enclosed in angle brackets `<word>` exactly as-is. "
            "Do not modify, remove, or reformat."
        )

        return self.escalation_manager.inject_into_prompt(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 _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.config_builder.build_tool_config(self.configs) if self.configs else {}
        parent_mcp_configs = self.config_builder.build_mcp_config(self.configs) if self.configs else {}

        # Also build configs for tools used by sub-agents that don't have configs yet
        if self.configs 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.config_builder.build_tool_config(self.configs, all_tools)

        # Also build configs for MCPs used by sub-agents that don't have configs yet
        if self.configs 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.config_builder.build_mcp_config(self.configs, 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.config_builder.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,
            skills=self.skills,
            model=self.model,
            agent_config=self.agent_config,
        )
