import datetime
import inspect
import re
import warnings
from typing import Callable, TypeVar, Any, Optional, Type, List, Dict
from unittest.mock import MagicMock

SwaggerObj = TypeVar("SwaggerObj", bound="swagger_mock")


def _get_attr_name_simple_check(line: str) -> Optional[str]:
    """
    line = if not patient_metadata_filters:
    """
    match = re.search(r'if (\w+) not in allowed_values:', line)
    if match:
        return match.group(1)
    return None


def _get_attr_name_set_check(line: str) -> Optional[str]:
    """
    line = if not set(patient_metadata_filters).issubset(set(allowed_values)):
    """
    match = re.search(r'if not set\((\w+)\).issubset\(set\(allowed_values\)\):', line)
    if match:
        return match.group(1)
    return None


def _get_allowed_values(cls: Type) -> Optional[Dict[str, List[Any]]]:
    """
    Match blocks like and extract allowed values from them

    allowed_values = ["train", "predict"]  # noqa: E501
    if <attr> not in allowed_values:
    """
    try:
        source = inspect.getsource(cls)
        attr_to_allowed_values = {}
        source_lines = source.split('\n')
        while source_lines:
            line = source_lines.pop(0)
            if 'allowed_values = ' in line:
                post_assignment = '='.join(line.split('=')[1:])
                to_evaluate = re.sub(r'#[^\'"]*$', '', post_assignment).strip()
                # pylint: disable=eval-used
                allowed_values = eval(to_evaluate)
                attr_check_line = source_lines.pop(0)
                if attr := _get_attr_name_simple_check(attr_check_line):
                    attr_to_allowed_values[attr] = allowed_values
                elif attr := _get_attr_name_set_check(attr_check_line):
                    attr_to_allowed_values[attr] = [allowed_values]
        return attr_to_allowed_values
    except TypeError:
        return None
    except OSError:
        return None


def _get_mock_value(argument_type: str, key: str, allowed_values: List[Any]) -> Any:
    # pylint: disable=too-many-return-statements
    if allowed_values:
        return allowed_values[0]
    if argument_type == 'str':
        return f'{key}_value'
    if argument_type == 'int':
        return 1
    if argument_type == 'float':
        return 1.0
    if argument_type == 'bool':
        return True
    if argument_type == 'datetime':
        return datetime.datetime.now()
    if argument_type == 'list':
        return []
    if argument_type == 'dict':
        return {}
    return MagicMock()


def swagger_mock(cls: Type[SwaggerObj], strict=False) -> Callable[..., SwaggerObj]:
    assert hasattr(cls, 'swagger_types'), \
        f'{cls} does not have swagger_types and must not be an autogenerated swagger object'
    swagger_types = cls.swagger_types
    allowed_values = _get_allowed_values(cls) or {}

    def constructor(**kwargs: Dict[str, Any]) -> SwaggerObj:
        for key, argument_type in swagger_types.items():
            if key not in kwargs:
                kwargs[key] = _get_mock_value(argument_type, key, allowed_values.get(key, None))
        try:
            return cls(**kwargs)
        except ValueError as e:
            if strict:
                raise e
            warnings.warn(f'Error creating {cls} with {kwargs}: {e}\n'
                          f'Creating a MagicMock instead. Unforeseen consequences may occur.')
            return MagicMock(spec=cls)
    return constructor
