#!/usr/bin/env python3

"""
Chaos testing framework for Genesis system.

This module provides a framework for randomly failing components
of the Genesis system to test its resilience.  It supports
injecting failures into various parts of the system and monitors
the overall health and stability during the chaos testing period.

The failures are injected randomly with a configurable probability.
The module also provides logging for all injected failures.
"""

import asyncio
import logging
import random
import time
from typing import Any, Callable, Coroutine, Dict, List, Optional

# Configure logging
logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)

# Define components that can be targeted for chaos testing
TARGETABLE_COMPONENTS = ["database", "api_server", "message_queue", "cache"]

# Default probability of failure injection
DEFAULT_FAILURE_PROBABILITY = 0.1


class ChaosTestFramework:
    """
    Framework for injecting chaos into the Genesis system.
    """

    def __init__(
        self,
        components: List[str],
        failure_probability: float = DEFAULT_FAILURE_PROBABILITY,
    ) -> None:
        """
        Initialize the ChaosTestFramework.

        Args:
            components: List of components to target for chaos testing.
            failure_probability: Probability of injecting a failure (0.0 to 1.0).
        """
        self.components = components
        self.failure_probability = failure_probability
        self.component_states: Dict[str, bool] = {
            component: True for component in components
        }  # True = healthy, False = failed

        # Validate the input parameters
        self._validate_inputs()

    def _validate_inputs(self) -> None:
        """
        Validate the input parameters to the ChaosTestFramework.
        Raises:
            ValueError: If any of the input parameters are invalid.
        """
        if not isinstance(self.components, list):
            raise ValueError("Components must be a list.")
        if not all(isinstance(component, str) for component in self.components):
            raise ValueError("All components must be strings.")
        for component in self.components:
            if component not in TARGETABLE_COMPONENTS:
                raise ValueError(
                    f"Component '{component}' is not a targetable component."
                )
        if not isinstance(self.failure_probability, (int, float)):
            raise ValueError("Failure probability must be a number.")
        if not 0.0 <= self.failure_probability <= 1.0:
            raise ValueError("Failure probability must be between 0.0 and 1.0.")

    def inject_failure(self, component: str) -> bool:
        """
        Inject a failure into the specified component.

        Args:
            component: Name of the component to fail.

        Returns:
            True if the failure was injected, False otherwise.
        """
        if not self.component_states[component]:  # already failed
            logging.info(f"Component {component} already failed, skipping injection.")
            return False
        if random.random() < self.failure_probability:
            logging.warning(f"Injecting failure into component: {component}")
            self.component_states[component] = False
            return True
        return False

    def restore_component(self, component: str) -> bool:
        """
        Restore a failed component to a healthy state.

        Args:
            component: Name of the component to restore.

        Returns:
            True if the component was restored, False otherwise.
        """
        if self.component_states[component]:  # Already healthy
            logging.info(f"Component {component} already healthy, skipping restore.")
            return False
        logging.info(f"Restoring component: {component}")
        self.component_states[component] = True
        return True

    def is_component_healthy(self, component: str) -> bool:
        """
        Check if a component is currently healthy.

        Args:
            component: Name of the component to check.

        Returns:
            True if the component is healthy, False otherwise.
        """
        return self.component_states[component]

    def run_chaos_loop(self, duration: int = 60) -> None:
        """
        Run the chaos testing loop for a specified duration.

        Args:
            duration: Duration of the chaos testing loop in seconds.
        """
        start_time = time.time()
        while time.time() - start_time < duration:
            component = random.choice(self.components)
            if self.is_component_healthy(component):
                self.inject_failure(component)
            else:
                self.restore_component(component)
            time.sleep(random.uniform(1, 5))  # Inject randomness in the timing too

    def get_component_states(self) -> Dict[str, bool]:
        """
        Get the current state of all monitored components.

        Returns:
            A dictionary mapping component names to their health status (True=healthy, False=failed).
        """
        return self.component_states.copy()  # Return a copy to prevent modification

    async def async_run_chaos_loop(self, duration: int = 60) -> None:
        """
        Run the chaos testing loop for a specified duration asynchronously.

        Args:
            duration: Duration of the chaos testing loop in seconds.
        """
        start_time = time.time()
        while time.time() - start_time < duration:
            component = random.choice(self.components)
            if self.is_component_healthy(component):
                self.inject_failure(component)
            else:
                self.restore_component(component)
            await asyncio.sleep(random.uniform(1, 5))  # Inject randomness in the timing too


# Example Usage (for testing)
if __name__ == "__main__":
    # Example usage
    chaos_tester = ChaosTestFramework(
        components=["database", "api_server"], failure_probability=0.2
    )
    logging.info("Starting chaos testing...")
    chaos_tester.run_chaos_loop(duration=10)  # Run for 10 seconds
    logging.info("Chaos testing finished.")
    print(f"Final Component States: {chaos_tester.get_component_states()}")