from __future__ import annotations
from dataclasses import dataclass
import random
import pandas as pd
from abc import ABC, abstractmethod


@dataclass
class Static_data_configuration_level:
    name: str
    feature: DataType


class DATA_TYPES:
    RANGE = "range"
    CHOICE = "choice"


class DataType(ABC):
    kind: str

    @abstractmethod
    def get_value(self):
        """Generates a value for this data type

        Returns
        -------
        [type]
            Value from the given type
        """

    @abstractmethod
    def get_values(self, n) -> list:
        """Generates `n` values for this data type

        Parameters
        ----------
        n : int
            Number of values to generate

        Returns
        -------
        list
            `n` random values"""


class RangeDataType(DataType):
    kind = "range"

    def __init__(
        self, max_range, min_range=0, as_integer: bool = False, **kwargs
    ) -> None:
        self.min_range = min_range
        self.max_range = max_range
        self.as_integer = as_integer
        self._delta = max_range - min_range

    def _get_rand_int(self):
        return random.randrange(
            self.min_range,
            self.max_range,
        )

    def _get_rand_float(self):
        return self.min_range + (random.random() * self._delta)

    def get_value(self):
        if self.as_integer:
            return self._get_rand_int()
        return self._get_rand_float()

    def get_values(self, n: int) -> list:
        return [self.get_value() for _ in range(n)]

    def to_dict(self):
        return {
            "kind": self.kind,
            "min_range": self.min_range,
            "max_range": self.max_range,
            "as_integer": self.as_integer,
        }


class ChoiceDataType(DataType):
    kind = "choice"

    def __init__(self, choices: list, **kwargs) -> None:
        self.choices = choices

    def get_value(self):
        return random.choice(self.choices)

    def get_values(self, n: int) -> list:
        return random.choices(self.choices, k=n)

    def to_dict(self) -> dict:
        return {"kind": self.kind, "choices": self.choices}


def get_data_type(level_configuration: dict) -> DataType:
    """DataType Factory according to provided `kind`.
    possible DataTypes:
    - Range
    - Choice

    Parameters
    ----------
    level_configuration : dict
        Static feature configuration

    Returns
    -------
    DataType
        DataType representing the configured feature

    Raises
    ------
    ValueError
        Provided kind is not in the available DATA_TYPES
    """
    kind = level_configuration.get("kind")
    if kind == DATA_TYPES.RANGE:
        return RangeDataType(**level_configuration)
    elif kind == DATA_TYPES.CHOICE:
        return ChoiceDataType(**level_configuration)
    raise ValueError(
        f"Configuration {kind} is not available, try {list(DATA_TYPES.__members__.keys())}"
    )


class Static_data_generator:
    def __init__(self, deployment: pd.DataFrame, configuration: dict) -> None:
        """Creates a static data generator according to givein configuration

        Parameters
        ----------
        deployment : pd.DataFrame
            Deployment dataframe, generated by the `deployment_generator`
        configuration : dict
            Configuration dictionary of static data or containing such at `static` entry
            Each entry can hold:
            - A list of optional values ex: [model_a, model_b, model_c], [1, 3, 5, 7, 11, 13]
            - A range string ex: range(10)
        """
        self.deployment = deployment.copy(deep=True)
        self.configuration: list = self._get_or_create_static_data_configuration(
            configuration
        )

    def _get_or_create_static_data_configuration(
        self, static_data_configuration: dict
    ) -> list:
        """Converts a static data configuration dictionary to a list of `Static_data_configuration_level`

        Parameters
        ----------
        static_data_configuration : dict
            Static data configuration

        Returns
        -------
        list[Static_data_configuration_level]
            a list of parsed `Static_data_configuration_level`
        """
        configuration = []
        if "static" in static_data_configuration:
            static_data_configuration = static_data_configuration["static"]
        for feature_name, feature_configuration in static_data_configuration.items():
            try:
                feature = get_data_type(feature_configuration)
                configuration.append(
                    Static_data_configuration_level(feature_name, feature)
                )
            except ValueError as e:
                continue
        return configuration

    def generate_static_data(self) -> pd.DataFrame:
        """Generates a dataframe with static data for the provided deployment
        according to the given configuration

        Returns
        -------
        pd.DataFrame
            DataFrame that contains the generated static data
        """
        deployment = self.deployment.copy()
        for static_feature in self.configuration:
            deployment[static_feature.name] = static_feature.feature.get_values(
                self.deployment.shape[0]
            )
        return deployment
