import random
from typing import Dict, Optional, Tuple

from gymnasium.core import ObsType
from pydantic import BaseModel

from primaite.game.agent.actions import ActionManager
from primaite.game.agent.interface import AbstractScriptedAgent
from primaite.game.agent.observations.observation_manager import ObservationManager
from primaite.game.agent.rewards import RewardFunction


class RandomAgent(AbstractScriptedAgent):
    """Agent that ignores its observation and acts completely at random."""

    def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]:
        """Sample the action space randomly.

        :param obs: Current observation for this agent, not used in RandomAgent
        :type obs: ObsType
        :param timestep: The current simulation timestep, not used in RandomAgent
        :type timestep: int
        :return: Action formatted in CAOS format
        :rtype: Tuple[str, Dict]
        """
        return self.action_manager.get_action(self.action_manager.space.sample())


class PeriodicAgent(AbstractScriptedAgent):
    """Agent that does nothing most of the time, but executes application at regular intervals (with variance)."""

    class Settings(BaseModel):
        """Configuration values for when an agent starts performing actions."""

        start_step: int = 20
        "The timestep at which an agent begins performing it's actions."
        start_variance: int = 5
        "Deviation around the start step."
        frequency: int = 5
        "The number of timesteps to wait between performing actions."
        variance: int = 0
        "The amount the frequency can randomly change to."
        max_executions: int = 999999
        "Maximum number of times the agent can execute its action."

    def __init__(
        self,
        agent_name: str,
        action_space: ActionManager,
        observation_space: ObservationManager,
        reward_function: RewardFunction,
        settings: Optional[Settings] = None,
    ) -> None:
        """Initialise PeriodicAgent."""
        super().__init__(
            agent_name=agent_name,
            action_space=action_space,
            observation_space=observation_space,
            reward_function=reward_function,
        )
        self.settings = settings or PeriodicAgent.Settings()
        self._set_next_execution_timestep(timestep=self.settings.start_step, variance=self.settings.start_variance)
        self.num_executions = 0

    def _set_next_execution_timestep(self, timestep: int, variance: int) -> None:
        """Set the next execution timestep with a configured random variance.

        :param timestep: The timestep when the next execute action should be taken.
        :type timestep: int
        :param variance: Uniform random variance applied to the timestep
        :type variance: int
        """
        random_increment = random.randint(-variance, variance)
        self.next_execution_timestep = timestep + random_increment

    def get_action(self, obs: ObsType, timestep: int) -> Tuple[str, Dict]:
        """Do nothing, unless the current timestep is the next execution timestep, in which case do the action."""
        if timestep == self.next_execution_timestep and self.num_executions < self.settings.max_executions:
            self.num_executions += 1
            self._set_next_execution_timestep(timestep + self.settings.frequency, self.settings.variance)
            return "NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0}

        return "DONOTHING", {}
