import mimetypes
import re
from typing import (
    Any,
    Union,
    Tuple,
    Optional,
    Dict,
    List,
)  # noqa: F401 # pylint: disable=unused-import

from pykechain.enums import PropertyVTypes
from pykechain.models.validators.mime_types_defaults import predefined_mimes
from pykechain.models.validators.validator_schemas import (
    filesizevalidator_schema,
    fileextensionvalidator_schema,
)
from pykechain.models.validators.validators_base import PropertyValidator
from pykechain.utils import EMAIL_REGEX_PATTERN


class NumericRangeValidator(PropertyValidator):
    """
    A numeric range validator, which validates a number between a range.

    The range validates positively upto and **including** the minvalue and maxvalue.

    An added ability is the check if the number conforms to a step within that range.
    The validation checks for both integer and floats. The stepsize is only enforced when the :attr:`enforce_stepsize`
    is set to `True`. This enforcement is accurate to an accuracy set in the :const:`.accuracy`
    (normally set to be 1E-6).

    .. versionadded:: 2.2

    :ivar minvalue: minimum value of the range
    :type minvalue: float or int
    :ivar maxvalue: maximum value of the range
    :type maxvalue: float or int
    :ivar stepsize: stepsize
    :type stepsize: float or int
    :ivar enforce_stepsize: flag to ensure that the stepsize is enforced
    :type enforce_stepsize: bool

    Examples
    --------
    >>> validator = NumericRangeValidator(minvalue=0, maxvalue=50)
    >>> validator.is_valid(42)
    True
    >>> validator.is_valid(50)
    True
    >>> validator.is_valid(50.0001)
    False
    >>> validator.is_valid(-1)
    False
    >>> validator.get_reason()
    Value '-1' should be between 0 and 50

    >>> stepper = NumericRangeValidator(stepsize=1000, enforce_stepsize=True)
    >>> stepper.is_valid(2000)
    True

    """

    vtype = PropertyVTypes.NUMERICRANGE

    def __init__(
        self,
        json=None,
        minvalue=None,
        maxvalue=None,
        stepsize=None,
        enforce_stepsize=None,
        **kwargs,
    ):
        """Construct the numeric range validator."""
        super().__init__(json=json, **kwargs)

        if minvalue is not None:
            self._config["minvalue"] = minvalue
        if maxvalue is not None:
            self._config["maxvalue"] = maxvalue
        if stepsize is not None:
            self._config["stepsize"] = stepsize
        if enforce_stepsize is not None:
            self._config["enforce_stepsize"] = enforce_stepsize

        if self._config.get("minvalue") is None:
            self.minvalue = float("-inf")
        else:
            self.minvalue = self._config.get("minvalue")
        if self._config.get("maxvalue") is None:
            self.maxvalue = float("inf")
        else:
            self.maxvalue = self._config.get("maxvalue")

        self.stepsize = self._config.get("stepsize", None)
        self.enforce_stepsize = self._config.get("enforce_stepsize", None)

        if self.minvalue > self.maxvalue:
            raise Exception(
                "The minvalue ({}) should be smaller than the maxvalue ({}) of the numeric "
                "range validation".format(self.minvalue, self.maxvalue)
            )
        if self.enforce_stepsize and self.stepsize is None:
            raise Exception("The stepsize should be provided when enforcing stepsize")

    def _logic(self, value: Any = None) -> Tuple[Union[bool, None], str]:
        basereason = f"Value '{value}' should be between {self.minvalue} and {self.maxvalue}"
        self._validation_result, self._validation_reason = None, "No reason"

        if value is not None:
            self._validation_result = value >= self.minvalue and value <= self.maxvalue
            if not self._validation_result:
                self._validation_reason = basereason
            else:
                self._validation_reason = basereason.replace("should be", "is")

        if self.stepsize != 1 and self.enforce_stepsize:
            # to account also for floating point stepsize checks: https://stackoverflow.com/a/30445184/246235
            if self.minvalue == float("-inf"):
                self._validation_result = (
                    abs(value / self.stepsize - round(value / self.stepsize)) < self.accuracy
                )
            else:
                self._validation_result = (
                    abs(
                        (value - self.minvalue) / self.stepsize
                        - round((value - self.minvalue) / self.stepsize)
                    )
                    < self.accuracy
                )

            if not self._validation_result:
                self._validation_reason = (
                    "Value '{}' is not in alignment with a stepsize of {}".format(
                        value, self.stepsize
                    )
                )

        return self._validation_result, self._validation_reason


class RequiredFieldValidator(PropertyValidator):
    """
    Required field validator ensures that a value is provided.

    Does validate all values. Does not validate `None` or `''` (empty string).

    .. versionadded:: 2.2

    Examples
    --------
    >>> validator = RequiredFieldValidator()
    >>> validator.is_valid("A value")
    True
    >>> validator.is_valid("")
    False
    >>> validator.is_valid(None)
    False
    >>> validator.get_reason()
    "Value is required"

    """

    vtype = PropertyVTypes.REQUIREDFIELD

    def _logic(self, value: Any = None) -> Tuple[Union[bool, None], str]:
        basereason = "Value is required"
        self._validation_result, self._validation_reason = None, "No reason"

        if (
            value is not None
            and value != ""
            and value != list()
            and value != tuple()
            and value != set()
        ):
            self._validation_result = True
            self._validation_reason = "Value is provided"
        else:
            self._validation_result = False
            self._validation_reason = basereason

        return self._validation_result, self._validation_reason


class BooleanFieldValidator(PropertyValidator):
    """A boolean field validator.

    This is a stub implementation. Should validate if a value is either 'truthy' or 'falsy'.
    """

    vtype = PropertyVTypes.BOOLEANFIELD


class EvenNumberValidator(PropertyValidator):
    """An even number validator that validates `True` when the number is even.

    Even numbers are scalar numbers which can be diveded by 2 and return a scalar. Floating point numbers are converted
    to integer first. So `int(4.5)` = 4.

    .. versionadded:: 2.2

    Example
    -------
    >>> validator = EvenNumberValidator()
    >>> validator.is_valid(4)
    True
    >>> validator.is_valid(4.5)  # float is converted to integer first
    True

    """

    vtype = PropertyVTypes.EVENNUMBER

    def _logic(self, value: Any = None) -> Tuple[Union[bool, None], str]:
        if value is None:
            self._validation_result, self.validation_reason = None, "No reason"
            return self._validation_result, self._validation_reason
        if not isinstance(value, (int, float)):
            self._validation_result, self.validation_reason = (
                False,
                "Value should be an integer, or float (floored)",
            )
            return self._validation_result, self._validation_reason

        basereason = f"Value '{value}' should be an even number"

        self._validation_result = int(value) % 2 < self.accuracy
        if self._validation_result:
            self._validation_reason = basereason.replace("should be", "is")
            return self._validation_result, self._validation_reason
        else:
            self._validation_reason = basereason
            return self._validation_result, self._validation_reason


class OddNumberValidator(PropertyValidator):
    """A odd number validator that validates `True` when the number is odd.

    Example
    -------
    >>> validator = OddNumberValidator()
    >>> validator.is_valid(3)
    True
    >>> validator.is_valid(3.5)  # float is converted to integer first
    True

    """

    vtype = PropertyVTypes.ODDNUMBER

    def _logic(self, value: Any = None) -> Tuple[Union[bool, None], str]:
        if value is None:
            self._validation_result, self.validation_reason = None, "No reason"
            return self._validation_result, self._validation_reason
        if not isinstance(value, (int, float)):
            self._validation_result, self.validation_reason = (
                False,
                "Value should be an integer, or float (floored)",
            )
            return self._validation_result, self._validation_reason

        basereason = f"Value '{value}' should be a odd number"

        self._validation_result = int(value) % 2 > self.accuracy
        if self._validation_result:
            self._validation_reason = basereason.replace("should be", "is")
            return self._validation_result, self._validation_reason
        else:
            self._validation_reason = basereason
            return self._validation_result, self._validation_reason


class SingleReferenceValidator(PropertyValidator):
    """A single reference validator, ensuring that only a single reference is selected.

    .. versionadded:: 2.2
    """

    vtype = PropertyVTypes.SINGLEREFERENCE

    def _logic(self, value: Any = None) -> Tuple[Union[bool, None], str]:
        if value is None:
            self._validation_result, self.validation_reason = None, "No reason"
            return self._validation_result, self._validation_reason
        if not isinstance(value, (list, tuple, set)):
            self._validation_result, self.validation_reason = (
                False,
                "Value should be a list, tuple or set",
            )
            return self._validation_result, self._validation_reason

        self._validation_result = len(value) == 1 or len(value) == 0
        if self._validation_result:
            self._validation_reason = "A single or no value is selected"
            return self._validation_result, self._validation_reason
        else:
            self._validation_reason = "More than a single instance is selected"
            return self._validation_result, self._validation_reason


class RegexStringValidator(PropertyValidator):
    """
    A regular expression string validator.

    With a configured regex pattern, a string value is compared and matched against this pattern. If there is
    a positive match, the validator validates correctly.

    For more information on constructing regex strings, see the `python documentation`_, `regex101.com`_, or
    `regexr.com`_.

    .. versionadded:: 2.2

    :ivar pattern: the regex pattern to which the provided value is matched against.

    Example
    -------
    >>> validator = RegexStringValidator(pattern=r"Yes|Y|1|Ok")
    >>> validator.is_valid("Yes")
    True
    >>> validator.is_valid("No")
    False

    .. _python documentation: https://docs.python.org/2/library/re.html
    .. _regex101.com: https://regex101.com/
    .. _regexr.com: https://regexr.com/

    """

    vtype = PropertyVTypes.REGEXSTRING

    def __init__(self, json=None, pattern=None, **kwargs):
        """Construct an regex string validator.

        If no pattern is provided than the regexstring `'.+'` will be used, which matches all provided text with
        at least a single character. Does not match `''` (empty string).

        :param json: (optional) dict (json) object to construct the object from
        :type json: dict
        :param pattern: (optional) valid regex string, defaults to r'.+' which matches all text.
        :type text: basestring
        :param kwargs: (optional) additional kwargs to pass down
        :type kwargs: dict
        """
        super().__init__(json=json, **kwargs)

        if pattern is not None:
            self._config["pattern"] = pattern

        self.pattern = self._config.get("pattern", r".+")
        self._re = re.compile(self.pattern)

    def _logic(self, value: Any = None) -> Tuple[Union[bool, None], str]:
        if value is None:
            self._validation_result, self.validation_reason = None, "No reason"
            return self._validation_result, self._validation_reason

        basereason = f"Value '{value}' should match the regex pattern '{self.pattern}'"

        self._validation_result = re.match(self._re, value) is not None
        if not self._validation_result:
            self._validation_reason = basereason
        else:
            self._validation_reason = basereason.replace("should match", "matches")

        return self._validation_result, self._validation_reason


class EmailValidator(RegexStringValidator):
    """
    A email string validator.

    :cvar pattern: the email regex pattern to which the provided value is matched against.
    """

    pattern = EMAIL_REGEX_PATTERN

    def __init__(self, json=None, **kwargs):
        """Construct an email string validator.

        :param json: (optional) dict (json) object to construct the object from
        :type json: dict
        :param kwargs: (optional) additional kwargs to pass down
        :type kwargs: dict
        """
        super().__init__(json=json, pattern=self.pattern, **kwargs)


class AlwaysAllowValidator(PropertyValidator):
    """
    An always allow Validator.

    Will always return True.
    """

    vtype = PropertyVTypes.ALWAYSALLOW

    def _logic(self, value: Any = None) -> Tuple[Union[bool, None], str]:
        """Process the inner logic of the validator.

        The validation results are returned as tuple (boolean (true/false), reasontext)
        """
        return True, "Always True"


class FileSizeValidator(PropertyValidator):
    """A file size Validator.

    The actual size of the file cannot be checked in pykechain without downloading this from the server, hence
    when the validator is used inside an attachment property, the validator returns always being valid.

    :ivar max_size: maximum size to check
    :type max_size: Union[int,float]

    Example
    -------
    >>> validator = FileSizeValidator(max_size=100)
    >>> validator.is_valid(100)
    True
    >>> validator.is_valid(-1)
    False
    >>> validator.is_valid("attachments/12345678-1234-5678-1234-567812345678/some_file.txt")
    True
    >>> validator.get_reason()
    We determine the filesize of 'some_file.txt' to be valid. We cannot check it at this end.

    """

    vtype = PropertyVTypes.FILESIZE
    jsonschema = filesizevalidator_schema

    def __init__(
        self, json: Optional[Dict] = None, max_size: Optional[Union[int, float]] = None, **kwargs
    ):
        """Construct a file size validator.

        :param json: (optional) dict (json) object to construct the object from
        :type json: Optional[Dict]
        :param max_size: (optional) number that counts as maximum size of the file
        :type accept: Optional[Union[int,float]]
        :param kwargs: (optional) additional kwargs to pass down
        """
        super().__init__(json=json, **kwargs)
        if max_size is not None:
            if isinstance(max_size, (int, float)):
                self._config["maxSize"] = int(max_size)
            else:
                raise ValueError("`max_size` should be a number.")
        self.max_size = self._config.get("maxSize", float("inf"))

    def _logic(
        self, value: Optional[Union[int, float]] = None
    ) -> Tuple[Optional[bool], Optional[str]]:
        """Based on a filesize (numeric) or  filepath of the property (value), the filesize is checked."""
        if value is None:
            return None, "No reason"

        basereason = f"Value '{value}' should be of a size less then '{self.max_size}'"

        if isinstance(value, (int, float)):
            if int(value) <= self.max_size and int(value) >= 0:
                return True, basereason.replace("should", "is")
            else:
                return False, basereason

        return (
            True,
            f"We determine the filesize of '{value}' to be valid. We cannot check it at this end.",
        )


class FileExtensionValidator(PropertyValidator):
    """A file extension Validator.

    It checks the value of the property attachment against a list of acceptable mime types or file extensions.

    Example
    -------
    >>> validator = FileExtensionValidator(accept=[".png", ".jpg"])
    >>> validator.is_valid("picture.jpg")
    True
    >>> validator.is_valid("document.pdf")
    False
    >>> validator.is_valid("attachments/12345678-1234-5678-1234-567812345678/some_file.txt")
    False

    >>> validator = FileExtensionValidator(accept=["application/pdf",
    ...                                            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"])
    >>> validator.is_valid("document.pdf")
    True
    >>> validator.is_valid("comma-separated-values.csv")
    False
    >>> validator.is_valid("attachments/12345678-1234-5678-1234-567812345678/modern_excel.xlsx")
    True

    """

    vtype = PropertyVTypes.FILEEXTENSION
    jsonschema = fileextensionvalidator_schema
    mimetype_regex = r"^[-\w.]+/[-\w.\*]+$"

    def __init__(
        self, json: Optional[Dict] = None, accept: Optional[Union[str, List[str]]] = None, **kwargs
    ):
        """Construct a file extension validator.

        :param json: (optional) dict (json) object to construct the object from
        :type json: Optional[Dict]
        :param accept: (optional) list of mimetypes or file extensions (including a `.`, eg `.csv`, `.pdf`)
        :type accept: Optional[List[Text]]
        :param kwargs: (optional) additional kwargs to pass down
        """
        super().__init__(json=json, **kwargs)
        if accept is not None:
            if isinstance(accept, str):
                self._config["accept"] = accept.split(",")
            elif isinstance(accept, List):
                self._config["accept"] = accept
            else:
                raise ValueError("`accept` should be a commaseparated list or a list of strings.")

        self.accept = self._config.get("accept", None)
        self._accepted_mimetypes = self._convert_to_mimetypes(self.accept)

    def _convert_to_mimetypes(self, accept: List[str]) -> Optional[List[str]]:
        """
        Convert accept array to array of mimetypes.

        1. convert accept list to array of mimetypes
        2. convert aggregator (if inside mime_types_defaults) to list of mimetypes

        :param accept: list of mimetypes or extensions
        :type accept: List[Text]
        :return: array of mimetypes:
        :rtype: List[Text]
        """
        if accept is None:
            return None

        marray = []
        for item in accept:
            # check if the item in the accept array is a mimetype on its own.
            if re.match(self.mimetype_regex, item):
                if item in predefined_mimes:
                    marray.extend(predefined_mimes.get(item))
                else:
                    marray.append(item)
            else:
                # we assume this is an extension.
                # we can only guess a url, we make a url like: "file.ext" to check.
                fake_filename = f"file{item}" if item.startswith(".") else f"file.{item}"

                # do guess
                guess, _ = mimetypes.guess_type(fake_filename)
                marray.append(guess) if guess is not None else print(guess)

        return marray

    def _logic(self, value: Optional[str] = None) -> Tuple[Optional[bool], Optional[str]]:
        """Based on the filename of the property (value), the type is checked.

        1. convert filename to mimetype
        3. check if the filename is inside the array of mimetypes. self._accepted_mimetypes
        """
        if value is None:
            return None, "No reason"

        basereason = f"Value '{value}' should match the mime types '{self.accept}'"

        guessed_type, _ = mimetypes.guess_type(value)
        if guessed_type is None:
            return False, f"Could not determine the mimetype of '{value}'"
        elif guessed_type in self._accepted_mimetypes:
            return True, basereason.replace("match", "matches")

        return False, basereason
