import importlib
import inspect
import random
import string
from typing import Type, List, Dict

import django.db.models as django_models
from otree.api import Page, BasePlayer, Submission


def gen_random_data(
    field_name, player_class, player, field_values: Dict[str, object] = None
):
    field: django_models.Field = player_class._meta.get_field(field_name)
    # default value
    if field_values and (field_name := str(field.attname)) in field_values:
        return field_values[field_name]
    # if blank allowed, leave blank with chance of 50%
    if field.blank:
        if random.randint(0, 1) == 0:
            return None
    try:
        # dynamic choices
        choices = getattr(player, f"{field_name}_choices")()
    except AttributeError:
        # static choices
        choices = field.choices
    # if choices, pick a random one
    if choices:
        choice = random.choice(choices)
        # return the first element of choice if field.choices follows the standard format:
        # [(A, B), (A, B) ...] where A is the actual value to be set on the model, and B is the human-readable name
        # (see https://docs.djangoproject.com/en/3.1/ref/models/fields/#choices)
        if type(choice) in (tuple, list) and len(choice) == 2:
            return choice[0]
        # otherwise return choice, although this is likely to break things
        return choice
    # if number, pick a random number between bounds
    # (use django fields which are the base class for oTree fields)
    # TODO change to get_min?
    if isinstance(field, django_models.IntegerField):
        min, max = getattr(field, "min", 0) or 0, getattr(field, "max", 10) or 10
        return random.randint(min, max)
    # elif string, generate random string, respecting minimum length
    # TODO add min length
    elif isinstance(field, django_models.TextField):
        chars = string.ascii_letters + string.punctuation
        return "".join(random.choice(chars) for _ in range(20))
    # TODO else raise error
    raise NotImplementedError


class AutoBot:
    """AutoBot fills in oTree fields automatically.

    Abstract Mixin.  Usage:
    ```
    PlayerBot(Bot, AutoBot)
    ```
    where `Bot` comes from the module __init__.py.

    For each round uses the `page_sequence` from the apps page module,
    unless you specify a `custom_page_sequence` attribute, with one page sequence per round.

    To manually specify data used in testing, including to overcome the blow limitations,
    use the `custom_field_values` attribute, with one set of values per entry.

    TODO Page form fields

    TODO Limitations:
    - only some fields
    - min/max
    - custom clean method
    - bad choices
    - player fields only
    """

    custom_page_sequence: Dict[int, List[Page]]
    custom_field_values: List[Dict[str, object]]

    def play_round(self):
        assert hasattr(self, "PlayerClass"), (
            "AutoBot needs to be used as a mixin, " "e.g. class PlayerBot(Bot, AutoBot)"
        )

        # module of app in which the Autobot is used (subclassed)
        player_class: Type[BasePlayer] = self.PlayerClass
        models_module = inspect.getmodule(player_class)

        field_values = (
            random.choice(self.custom_field_values)
            if hasattr(self, "custom_field_values")
            else None
        )

        # page_sequence of the app
        page_sequence: List[Page]
        if hasattr(self, "custom_page_sequence"):
            page_sequence = self.custom_page_sequence[self.round_number]
        else:
            # all pages
            pages_module = importlib.import_module(f"{models_module.__package__}.pages")
            page_sequence = pages_module.page_sequence

        for page in page_sequence:
            random_data, fields = {}, None
            if hasattr(page, "bot_form_fields"):
                assert page.form_model == "player"
                fields = page.bot_form_fields(self)
            elif hasattr(page, "form_fields") and page.form_fields:
                assert page.form_model == "player"
                fields = page.form_fields
            # TODO warning
            if fields:
                random_data = {
                    field: gen_random_data(
                        field,
                        player_class,
                        self.player,
                        field_values,
                    )
                    for field in fields
                }
                # remove empty fields (oTree does not allow None, even if blank==True)
                random_data = {k: v for k, v in random_data.items() if v is not None}
            yield Submission(page, random_data)
