from abc import ABC, abstractmethod
from datetime import datetime
from typing import Any, Callable, Dict, List, Optional, Tuple, Union

from pydantic.main import BaseModel

from cicadad.util.constants import ONE_SEC_MS


class Result(BaseModel):
    """Result generated by a user or scenario."""

    id: Optional[str]
    output: Optional[Any]
    exception: Optional[Any]
    logs: Optional[str]
    timestamp: Optional[datetime]
    time_taken: Optional[float]
    succeeded: Optional[int]
    failed: Optional[int]

    class Config:
        """Encode class as JSON."""

        json_encoders = {
            Exception: lambda e: str(e),
        }


class UserEvent(BaseModel):
    """Store JSON encoded event sent to users."""

    kind: str
    payload: dict


class IScenarioCommands(ABC):
    """Interface to decouple scenario commands from scenario."""

    @property
    @abstractmethod
    def test_id(self) -> str:
        """Get ID of current test."""
        pass

    @property
    @abstractmethod
    def scenario_id(self) -> str:
        """Get ID of current scenario."""
        pass

    @property
    @abstractmethod
    def context(self) -> dict:
        """Get test context at the time scenario was started"""
        pass

    @property
    @abstractmethod
    def aggregated_results(self) -> Any:
        """Get result based on all results gathered from users."""
        pass

    @aggregated_results.setter
    def aggregated_results(self, val: Any):
        pass

    @property
    @abstractmethod
    def num_users(self) -> int:
        """Get number of users currently running in scenario."""
        pass

    @property
    @abstractmethod
    def num_results_collected(self) -> int:
        """Get number of results collected from users."""
        pass

    @property
    @abstractmethod
    def errors(self) -> List[str]:
        """List of errors reported by users."""
        pass

    @abstractmethod
    def scale_users(self, n: int):
        """Change number of running users.

        Args:
            n (int): Desired number of users
        """

    @abstractmethod
    def start_users(self, n: int):
        """Start users for a scenario.

        Args:
            n (int): number of users to start
        """
        pass

    @abstractmethod
    def stop_users(self, n: int):
        """Stop users for a scenario.

        Args:
            n (int): number of users to stop
        """
        pass

    @abstractmethod
    def add_work(self, n: int):
        """Distribute work (iterations) to all users in scenario.

        Args:
            n (int): Amount of work to distribute across user pool
        """
        pass

    @abstractmethod
    def send_user_events(self, kind: str, payload: dict):
        """Send an event to all user in the user pool.

        Args:
            kind (str): Type of event
            payload (dict): JSON dict to send to user
        """
        pass

    @abstractmethod
    def get_latest_results(
        self,
        timeout_ms: Optional[int] = ONE_SEC_MS,
        limit: int = 500,
    ) -> List[Result]:
        """Gathers results produced by users.

        Args:
            timeout_ms (int, optional): Time to wait for results. Defaults to 1000.
            limit (int): Max results to return. Defaults to 500

        Returns:
            List[Result]: List of latest results collected
        """
        pass

    @abstractmethod
    def aggregate_results(self, latest_results: List[Result]) -> Any:
        """Run scenario aggregator function against latest gathered results and save aggregate.

        Args:
            latest_results (List[Result]): Results to run aggregator function on

        Returns:
            Any: Result of scenario aggregator function
        """
        pass

    @abstractmethod
    def verify_results(self, latest_results: List[Result]) -> Optional[List[str]]:
        """Run scenario result verification function against latest results.

        Args:
            latest_results (List[Result]): Last results to be collected

        Returns:
            Optional[List[str]]: List of error strings gathered for scenario
        """
        pass

    @abstractmethod
    def collect_datastore_metrics(self, latest_results: List[Result]):
        """Parse latest results and save metrics if any can be parsed from result set.

        Args:
            latest_results (List[Result]): List of latest collected results
        """
        pass


class IUserCommands(ABC):
    """Interface to decouple user commands from scenario."""

    @property
    @abstractmethod
    def user_id(self) -> str:
        """Get current user id."""
        pass

    @abstractmethod
    def get_events(self, kind: str):
        """Get events for current user."""
        pass

    @abstractmethod
    def is_up(self) -> bool:
        """Check if user is still running.

        Returns:
            bool: User is up
        """
        pass

    @abstractmethod
    def has_work(self, timeout_ms: Optional[int] = ONE_SEC_MS) -> bool:
        """Check if user has remaining invocations.

        Args:
            timeout_ms (int, optional): Time to wait for work event to appear before returning. Defaults to 1000.

        Returns:
            bool: User has work
        """
        pass

    @abstractmethod
    def run(self, *args, log_traceback=True, **kwargs) -> Tuple[Any, Exception, str]:
        """Run scenario function with arguments; capture exception and logs.

        Args:
            log_traceback (bool, optional): Print out traceback for exception. Defaults to True.

        Returns:
            Tuple[Any, Exception, str]: Output, exception, and logs captured
        """
        pass

    @abstractmethod
    def report_result(
        self, output: Any, exception: Any, logs: Optional[str], time_taken: float
    ):
        """Report result for scenario invocation from user to scenario.

        Args:
            output (Any): Function output
            exception (Any): Function exception
            logs (Optional[str]): Function logs
            time_taken (float): Time taken in seconds to call function once
        """
        pass


class TestStatus(BaseModel):
    """Store status payload reported by test."""

    scenario: Optional[str]
    scenario_id: Optional[str]
    message: str
    context: Optional[str]


class ScenarioMetric(BaseModel):
    """Store metrics payload reported by test."""

    scenario: str
    metrics: Dict[str, Optional[str]]


class TestEvent(BaseModel):
    """Store picled event from test to CLI."""

    kind: str
    payload: Union[TestStatus, ScenarioMetric]


class ICLIBackend(ABC):
    """Backend methods available to CLI."""

    @abstractmethod
    def create_test(
        self,
        scheduling_metadata: str,
        backend_address: str,
        tags: List[str],
        env: Dict[str, str],
    ) -> str:
        """Create a test instance.

        Args:
            scheduling_metadata (str): JSON string containing scheduling metadata for backend scheduling implementation
            backend_address (str): Address of backend within instance
            tags (List[str]): Tags to filter scenarios under test.

        Returns:
            str: Created test id
        """
        pass

    @abstractmethod
    def get_test_events(self, test_id: str) -> List[TestEvent]:
        """Get events from test.

        Args:
            test_id (str): Test ID to retrieve from
        """
        pass

    @abstractmethod
    def clean_test_instances(self, test_id: str):
        """Stop instances created by test.

        Args:
            test_id (str): Test ID to clean instances under
        """
        pass


class IConsoleMetricsBackend(ABC):
    """Backend methods available to console metrics."""

    @abstractmethod
    def get_metric_statistics(self, scenario_id: str, name: str) -> Optional[dict]:
        """Get statistic breakdown for metric in datastore.

        Args:
            scenario_id (str): scenario to retrieve metrics for
            name (str): name of metric
        """
        pass

    @abstractmethod
    def get_metric_total(self, scenario_id: str, name: str) -> Optional[float]:
        """Get total value of metrics with this name.

        Args:
            scenario_id (str): scenario to retrieve metrics for
            name (str): name of metric
        """
        pass

    @abstractmethod
    def get_last_metric(self, scenario_id: str, name: str) -> Optional[float]:
        """Get last added value for this metric.

        Args:
            scenario_id (str): scenario to retrieve metrics for
            name (str): name of metric
        """
        pass

    @abstractmethod
    def get_metric_rate(
        self, scenario_id: str, name: str, split_point: float
    ) -> Optional[float]:
        """Get percent of metric above split point.

        Args:
            scenario_id (str): scenario to retrieve metrics for
            name (str): name of metric
            split_point (float): get metrics over this value
        """
        pass


class ITestBackend(ABC):
    """Decouples backend from calling test methods."""

    @abstractmethod
    def create_scenario(
        self,
        scenario_name: str,
        context: str,
        users_per_instance: int,
        tags: List[str],
    ) -> str:
        """Start a scenario instance.

        Args:
            scenarioName (str): Name of scenario to start
            context (str): Base64 encoded JSON context to pass to scenario (and users)
            usersPerInstance (int): Number of users per instance created by scenario
            tags (List[str]): Filtering tags for scenario

        Returns:
            str: Scenario ID created
        """
        pass

    @abstractmethod
    def add_test_event(self, event: TestEvent):
        """Send event from test.

        Args:
            event (TestEvent): Event to send
        """
        pass

    @abstractmethod
    def move_scenario_result(self, scenario_id: str) -> Optional[dict]:
        """Get result for scenario.

        Args:
            scenario_id (str): ID of scenario to get
        """
        # FEATURE: see if this can be changed to return "Result"
        pass

    @abstractmethod
    def get_console_metrics_backend(self) -> IConsoleMetricsBackend:
        """Get configured backend for created users.

        Returns:
            IUserBackend: Backend for users
        """
        pass

    @abstractmethod
    def scenario_running(self, scenario_id: str) -> bool:
        """Check if scenario is running

        Args:
            scenario_id (str): ID of scenario to check

        Returns:
            bool: Scenario is running or not
        """
        pass


class IScenarioBackend(ABC):
    """Backend methods available to scenario."""

    @abstractmethod
    def create_users(self, amount: int) -> List[str]:
        """Create users for scenario.

        Args:
            amount (int): Number of users to create

        Returns:
            List[str]: List of created user manager IDs
        """
        pass

    @abstractmethod
    def stop_users(self, amount: int):
        """Stop users in scenario.

        Args:
            amount (int): Number of users to stop
        """
        pass

    @abstractmethod
    def distribute_work(self, n: int):
        """Send work to users in scenario.

        Args:
            n (int): Amount of work to distribute
        """
        pass

    @abstractmethod
    def send_user_events(self, kind: str, payload: dict):
        """Send events to users in scenario.

        Args:
            kind (str): Type of event to send
            payload (dict): Body of event
        """
        pass

    @abstractmethod
    def move_user_results(
        self, limit: int, timeout_ms: Optional[int] = ONE_SEC_MS
    ) -> List[Result]:
        """Get user results from datastore.

        Args:
            limit (int): Limit of results to capture
            timeout_ms (int, optional): Time to wait for work event to appear before returning. Defaults to 1000.

        Returns:
            List[Result]: User results gathered from datastore
        """
        pass

    @abstractmethod
    def set_scenario_result(
        self,
        output: Any,
        exception: Any,
        logs: str,
        time_taken: float,
        succeeded: int,
        failed: int,
    ):
        """Set result for scenario after completion.

        Args:
            output (Any): Scenario output
            exception (Any): Exception that occured when running scenario
            logs (str): Logs generated by scenario
            time_taken (float): Elapsed time running scenario
            succeeded (int): Amount of succeeded attempts
            failed (int): Amount of failed attempts
        """
        pass

    @abstractmethod
    def add_metric(self, name: str, value: float):
        """Send metric to from scenario to datastore.

        Args:
            name (str): Name of metric to send
            value (float): Numeric value to report
        """
        pass


class IUserBackend(ABC):
    """Datastore methods available to user."""

    @abstractmethod
    def get_user_events(self, kind: str) -> List[UserEvent]:
        """Get events for current user.

        Args:
            kind (str): Type of event to get

        Returns:
            List[UserEvent]: Events of this type for the user
        """
        pass

    @abstractmethod
    def get_work(self, timeout_ms: Optional[int] = ONE_SEC_MS) -> int:
        """Get work for current user.

        Args:
            user_id (str): [description]

        Returns:
            int: Amount of new work for user
        """
        pass

    @abstractmethod
    def add_user_result(self, result: Result):
        """Report cycle result for user.

        Args:
            result (Result): Result to report
        """
        pass


class IUserManagerBackend(ABC):
    """Interface to decouple backend commands for user manager."""

    @abstractmethod
    def get_new_users(self) -> List[str]:
        """Get events for current user manager.

        Returns:
            List[str]: New user IDs created
        """
        pass

    @abstractmethod
    def get_user_backend(self, user_id: str) -> IUserBackend:
        """Get configured backend for created users.

        Returns:
            IUserBackend: Backend for users
        """
        pass

    @abstractmethod
    def send_user_results(self):
        """Flush buffer full of user results."""
        pass


UserLoopFn = Callable[[IUserCommands, dict], None]
LoadModelFn = Callable[[IScenarioCommands, dict], Any]
# FEATURE: Give result aggregator access to metrics and num users for scenario
ResultAggregatorFn = Callable[[Optional[Any], List[Result]], Any]
ResultVerifierFn = Callable[[List[Result]], List[str]]
OutputTransformerFn = Callable[[Optional[Any]], Any]
MetricCollector = Callable[[List[Result], IScenarioBackend], None]
ConsoleMetricDisplays = Dict[
    str, Callable[[str, str, IConsoleMetricsBackend], Optional[str]]
]
