import re
from string import Formatter
from typing import Union, List, overload, Tuple, Iterator, Dict, Any

from spotlight import rules as rls, config
from spotlight.exceptions import RuleNotFoundError, InvalidDataError, InvalidRulesError
from spotlight.utils import get_field_value


class Validator:
    """
    Creates an instance of the Validator class.

    Parameters
    ----------
    plugins : list
        A list of plugins that add additional validation rules.
    """

    class Plugin:
        """
        Creates an instance of the Validator Plugin class.
        """

        def rules(self) -> List[rls.Rule]:
            return []

    def __init__(self, plugins: List[Plugin] = None):
        self.data = None
        self.rules = None
        self.output = {}
        self.config = config
        self._flat_list = []

        self.overwrite_messages = {}
        self.overwrite_fields = {}
        self.overwrite_values = {}

        self._implicit_rules: List[str] = []
        self._available_rules: Dict[str, rls.Rule] = {}

        self._setup_default_rules()
        self._setup_plugins(plugins or [])

    def _setup_default_rules(self):
        self.register_rules(self._default_rules())

    def register_rules(self, rules: [rls.Rule]):
        for rule in rules:
            self.register_rule(rule)

    def register_rule(self, rule: rls.Rule):
        self._setup_rule(rule)

    def _setup_rule(self, rule):
        self._available_rules[rule.name] = rule
        if rule.implicit:
            self._implicit_rules.append(rule.name)

    @staticmethod
    def _default_rules() -> List[rls.Rule]:
        return [
            rls.AcceptedRule(),
            rls.AfterRule(),
            rls.AlphaNumRule(),
            rls.AlphaNumSpaceRule(),
            rls.BeforeRule(),
            rls.BooleanRule(),
            rls.DateTimeRule(),
            rls.DictRule(),
            rls.EmailRule(),
            rls.FilledRule(),
            rls.FloatRule(),
            rls.InRule(),
            rls.IntegerRule(),
            rls.IpRule(),
            rls.JsonRule(),
            rls.ListRule(),
            rls.MaxRule(),
            rls.MinRule(),
            rls.NotWithRule(),
            rls.RequiredIfRule(),
            rls.RequiredRule(),
            rls.RequiredUnlessRule(),
            rls.RequiredWithRule(),
            rls.RequiredWithoutRule(),
            rls.StartsWithRule(),
            rls.StringRule(),
            rls.UrlRule(),
            rls.Uuid4Rule(),
        ]

    def _setup_plugins(self, plugins: List[Plugin]):
        for plugin in plugins:
            self.register_rules(plugin.rules())

    @staticmethod
    def _dynamically_add_static_validation_methods(rule):
        for attr in dir(rule):
            if attr.startswith("valid_"):
                validation_method = getattr(rule, attr)
                setattr(Validator, attr, staticmethod(validation_method))

    @overload
    def validate(
        self, data: dict, rules: dict, flat: bool = False
    ) -> Union[dict, list]:
        ...

    @overload
    def validate(
        self, data: object, rules: dict, flat: bool = False
    ) -> Union[dict, list]:
        ...

    def validate(
        self, data: Union[dict, object], rules: dict, flat: bool = False
    ) -> Union[dict, list]:
        """
        Validate data with given rules.

        Parameters
        ----------
        data : dict or object
            Dict or object that can be converted to a dict with data that needs
            to be validated.
            For example: {"email": "john.doe@example.com"},
            CustomDataClass(email="john.doe@example.com")
        rules : dict
            Dict with validation rules for the given data.
            For example: {"email": "required|email|unique:user,email"}
        flat : bool, optional
            Returns a list of errors instead of a dict if true.

        Returns
        -------
        errors : dict or list
            Dict or list of errors. When a dict of errors is returned, each key
            represents the field name, and the corresponding value is a list of
            errors for that key/field. When the optional parameter `flat` is
            set to true, a list of only the error messages is returned.
        """
        self.data = data
        self.rules = rules
        self.output = {}

        self._convert_data_to_dict()
        self._validate_rules_type()
        self._validate_data()

        if flat:
            self._flat_list = []
            self._flatten_output(self.output)

            return self._flat_list

        return self.output

    def _sub_fields(self, field) -> Iterator[str]:
        if not self._contains_wildcard(field):
            yield field
            return

        root = field.split(self.config.FIELD_WILD_CARD)[0].strip(
            self.config.FIELD_DELIMITER
        )
        root_value = self._get_field_value(root)

        if isinstance(root_value, list):
            for i, _ in enumerate(root_value):
                new_field = field.replace(self.config.FIELD_WILD_CARD, str(i), 1)
                yield from self._sub_fields(new_field)

    def _validate_data(self):
        # Iterate over fields
        for raw_field, rules in self._field_iterator():
            # Iterate over sub fields
            for field in self._sub_fields(raw_field):
                # Iterate over rules
                for rule_name, rule_parameters in self.rule_iterator(rules):
                    # Check if rule exists
                    if not self._rule_exists(rule_name):
                        raise RuleNotFoundError(rule_name)

                    # Check if field is validatable
                    if self._is_validatable(field, rule_name):
                        rule = self._available_rules.get(rule_name)
                        value = self._get_field_value(field)

                        # If rule didn't pass, add error
                        if not rule.passes(field, value, rule_parameters, self):
                            self._add_error(rule)

                            # Stop
                            if rule.stop:
                                break

    def _field_iterator(self) -> Iterator[Tuple[str, List[str]]]:
        for field, rules in self.rules.items():
            yield field, self._split_rules(rules)

    def rule_iterator(self, rules) -> Iterator[Tuple[str, List[str]]]:
        for rule in rules:
            yield self._rule_name(rule), self._rule_parameters(rule)

    def _split_rules(self, rules: str) -> List[str]:
        return rules.split(self.config.RULE_DELIMITER)

    def _rule_name(self, rule: str) -> str:
        return self._split_rule(rule)[0]

    def _rule_parameters(self, rule: str) -> List[str]:
        if self.config.RULE_PARAM_DELIMITER not in rule:
            return []

        return self._split_rule(rule)[1].split(self.config.RULE_PARAMS_DELIMITER)

    def _split_rule(self, rule: str) -> List[str]:
        return rule.split(self.config.RULE_PARAM_DELIMITER, 1)

    def _convert_data_to_dict(self):
        if not isinstance(self.data, dict):
            try:
                self.data = self.data.__dict__
            except AttributeError:
                raise InvalidDataError(type(self.data))

    def _validate_rules_type(self):
        if type(self.rules) is not dict:
            raise InvalidRulesError(type(self.rules))

    def _rule_exists(self, rule_name: str) -> bool:
        return rule_name in self._available_rules

    def _contains_wildcard(self, value) -> bool:
        return self.config.FIELD_WILD_CARD in value

    def _is_validatable(self, field: str, rule_name: str) -> bool:
        return self._field_is_present(field) or self._rule_is_implicit(rule_name)

    def _field_is_present(self, field: str) -> bool:
        return self._get_field_value(field) is not None

    def _rule_is_implicit(self, rule_name: str) -> bool:
        return rule_name in self._implicit_rules

    def _add_error(self, rule: rls.Rule):
        field = rule.message_fields.get(self.config.FIELD_KEY)
        error = self._create_error(rule)

        if field in self.output:
            self.output.get(field).append(error)
        else:
            self.output[field] = [error]

    @staticmethod
    def _find_fields_in_error(error: str, regex: bool = False) -> list:
        if regex:
            return re.findall("{(.*?)}", error)

        return [x[1] for x in Formatter().parse(error) if x[1] is not None]

    def _create_error(self, rule: rls.Rule):
        error = rule.message
        fields = rule.message_fields
        field = fields.get(self.config.FIELD_KEY)
        field = self._convert_field_to_wildcard_field(field)
        combined_field = field + self.config.FIELD_DELIMITER + rule.name

        # Overwrite values
        fields = self._overwrite_values(field, fields)

        # If present, format values
        if "values" in fields:
            fields["values"] = self._format_values(fields["values"])

        # Overwrite messages
        error = self._overwrite_messages(error, rule.name, field, combined_field)

        # Overwrite fields
        fields = self._overwrite_fields(fields)

        return error.format(**fields)

    def _overwrite_values(self, field, fields) -> dict:
        for key in ["value", "values"]:
            new_values = fields.get(key)
            field_overwrite_values = self.overwrite_values.get(field, [])

            # Values
            if isinstance(new_values, list):
                # Field
                self._overwrite_values_in_list(
                    fields, new_values, field_overwrite_values, key
                )
                # Global
                self._overwrite_values_in_list(
                    fields, new_values, self.overwrite_values, key
                )
            # Value
            elif isinstance(new_values, str):
                # Field
                self._overwrite_value(fields, new_values, field_overwrite_values, key)
                # Global
                self._overwrite_value(fields, new_values, self.overwrite_values, key)

            # Other field
            if "other" in fields:
                other = fields.get("other")
                field_overwrite_values = self.overwrite_values.get(other, [])
                self._overwrite_value(fields, new_values, field_overwrite_values, key)

        return fields

    @staticmethod
    def _overwrite_values_in_list(fields, new_values, overwrites, key):
        for index, value in enumerate(new_values):
            if value in overwrites:
                fields[key][index] = overwrites.get(value)

    @staticmethod
    def _overwrite_value(fields, new_values, overwrites, key):
        if new_values in overwrites:
            fields[key] = overwrites.get(new_values)

    @staticmethod
    def _format_values(values: List[str]) -> str:
        return ", ".join(values)

    def _overwrite_messages(self, error, *fields):
        for field in fields:
            if field in self.overwrite_messages:
                error = self.overwrite_messages[field]

        return error

    def _overwrite_fields(self, fields):
        for overwrite_field, overwrite_value in self.overwrite_fields.items():
            for key, value in fields.items():
                value = self._convert_field_to_wildcard_field(value)
                if overwrite_field == value:
                    fields[key] = overwrite_value

        return fields

    def _convert_field_to_wildcard_field(self, field) -> str:
        split_fields = str(field).split(self.config.FIELD_DELIMITER)
        for index, field in enumerate(split_fields):
            if field.isnumeric():
                split_fields[index] = self.config.FIELD_WILD_CARD

        return self.config.FIELD_DELIMITER.join(split_fields)

    def _flatten_output(self, output: dict):
        for field, errors in output.items():
            for error in errors:
                self._flat_list.append(error)

    def _get_field_value(self, field) -> Any:
        return get_field_value(self.data, field)

    def field_rules(self, field: str) -> List[str]:
        return self._split_rules(self.rules.get(field))

    @staticmethod
    def valid_email(value: Any) -> bool:
        return rls.EmailRule.valid_email(value)

    @staticmethod
    def valid_url(value: Any) -> bool:
        return rls.UrlRule.valid_url(value)

    @staticmethod
    def valid_ip(value: Any) -> bool:
        return rls.IpRule.valid_ip(value)

    @staticmethod
    def valid_uuid4(value: Any) -> bool:
        return rls.Uuid4Rule.valid_uuid4(value)

    @staticmethod
    def valid_string(value: Any) -> bool:
        return rls.StringRule.valid_string(value)

    @staticmethod
    def valid_integer(value: Any) -> bool:
        return rls.IntegerRule.valid_integer(value)

    @staticmethod
    def valid_float(value: Any) -> bool:
        return rls.FloatRule.valid_float(value)

    @staticmethod
    def valid_boolean(value: Any) -> bool:
        return rls.BooleanRule.valid_boolean(value)

    @staticmethod
    def valid_json(value: Any) -> bool:
        return rls.JsonRule.valid_json(value)

    @staticmethod
    def valid_alpha_num(value: Any) -> bool:
        return rls.AlphaNumRule.valid_alpha_num(value)

    @staticmethod
    def valid_alpha_num_space(value: Any) -> bool:
        return rls.AlphaNumSpaceRule.valid_alpha_num_space(value)

    @staticmethod
    def valid_list(value: Any) -> bool:
        return rls.ListRule.valid_list(value)

    @staticmethod
    def valid_dict(value: Any) -> bool:
        return rls.DictRule.valid_dict(value)

    @staticmethod
    def valid_date_time(value: Any, date_time_format: str = None) -> bool:
        return rls.DateTimeRule.valid_date_time(value, date_time_format)
