import re
import string
from typing import Optional, Sequence, Union

from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import (
    URLValidator,
    ProhibitNullCharactersValidator,
)
from django.utils.regex_helper import _lazy_re_compile as re_compile
from django_extensions.validators import NoControlCharactersValidator

from lcutils.networking import ValidIP

from console_base.utils import weekdays

PUNCTUATION = string.punctuation.replace('_', '')
ILLEGAL_PUNCTUATION = PUNCTUATION.replace('-', '')

# for some compatibility with LDAP usernames
# Do NOT allow '@' in username, since this would permit a full email address in the username field,
# and Person A could have for a username the email of Person B. The easiest way to prevent such
# confusion is to prevent the '@' as a character in usernames. For Active Directory purposes,
# perhaps some standardized transformation could be implemented to swap out the @ sign for saving
# in the database, yet restore the @ sign for comparing against Active Directory logins, etc.
USERNAME_LEGAL_PUNCTUATION = ('-', '.', '+')
chars = re_compile(r'[%s]' % re.escape(''.join([mark for mark in string.punctuation])))

VALIDATION_ERROR = Optional[ValidationError]


# ----------------------------------------------------------------------
class FieldValidator:
    """
    Check for common field errors and return message.
    Message string can be used in function-based validators
    to either return or raise a ValidationError
    """

    def __init__(self, value: str):
        self.value = value or ''

    # -----------------------------------------------------------------
    def is_ip_domain(self) -> str:
        """Should be valid IP, IP Network, or domain name / URL"""

        try:
            int(self.value.split('.')[0])
            is_ip = True
        except (IndexError, TypeError, ValueError):
            is_ip = False

        if is_ip:
            ip_error = self.valid_ip()
            if not ip_error:
                return ''
        else:
            domain_error = self.valid_domain()
            if not domain_error:
                return ''

        return 'Enter a valid domain domain or IP block'

    # -----------------------------------------------------------------
    def is_multiple_words(self) -> str:
        """
        Check for multi-word string
        length for comments field
        """
        if len(self.value.split()) < 2:
            return 'Please provide more detailed information'
        return ''

    # -----------------------------------------------------------------
    def legal_punctuation(self, skip: Sequence = ()) -> str:
        for char in ILLEGAL_PUNCTUATION:
            if char not in skip and char in self.value:
                legal = ', '.join([f'"{p}"' for p in skip])
                return f"Please don't use punctuation other than underscore or {legal}."

        return ''

    # -----------------------------------------------------------------
    def no_punctuation(self) -> str:
        """
        No punctuation allowed except for underscore "_"
        """
        for char in PUNCTUATION:
            if char in self.value:
                return f"'{char}' is illegal. Don't use any punctuation here."

        return ''

    # -----------------------------------------------------------------
    def no_spaces(self) -> str:
        if ' ' in self.value:
            return 'Please do not use spaces here.'
        return ''

    # -----------------------------------------------------------------
    def valid_regex(self) -> str:
        """
        If pattern is regex, make sure it's
        properly formed and sufficiently long:
        """

        if len(self.value) < 5:
            # Don't length-check extension regexes like /\.js/
            if self.value.startswith(('.', r'\.')):
                pass
            else:
                return f'"{self.value}" is too short, which causes unintended matches'

        try:
            re.compile(self.value)
        except Exception:  # noqa
            return 'Regex is malformed'

        if '/' in self.value:
            pos = self.value.index('/')
            if self.value[pos - 1] != "\\":
                return f"A backslash should precede the forward slash in {self.value}"

        return self.no_spaces()

    # -----------------------------------------------------------------
    def valid_ip(self) -> str:
        """
        Valid IP or CIDR notation
        """
        ip = ValidIP(self.value).valid_ip()
        try:
            network = ValidIP(self.value).is_network()
        except ValueError:
            network = False
        if ip or network:
            return ''

        return 'Not a valid IP or CIDR IP range'

    # -----------------------------------------------------------------
    def valid_domain(self, allow_www: bool = False) -> str:
        """
        If pattern is domain/url, make sure it's properly formed.
        """

        # default is a valid value for pattern.list files
        if self.value.lower() == 'default':
            return ''

        if self.value.startswith(('http://', 'https://', 'ftp://')):
            return f'Please remove schema from "{self.value}"'

        if not allow_www and self.value.startswith('www.'):
            return f'Please remove www. from "{self.value}"'

        # Check for valid URL strings per django's validator
        url = URLValidator()
        try:
            url(f'http://{self.value}')
        except (TypeError, ValidationError):
            return f'{self.value} is not a properly formatted domain name or URL'

        return self.no_spaces() or self.legal_punctuation(('.', '/', '-'))

    def valid_rpz_domain(self) -> str:
        """
        RPZ domains that match all subdomains need to start with '*.'
        Redwood doesn't have this requirement, so strip these chars
        before validating the domain.
        """
        if self.value.startswith('*.'):
            self.value = self.value[2:]

        if not self.valid_ip():
            return 'IP addresses are not valid for this RPZ'

        return self.valid_domain(allow_www=True)

    def valid_url_schema(self) -> str:
        if len(self.value) <= 10 and self.value.endswith((':*', '://', '://*')):
            return ''
        return f'{self.value} is not a valid URL schema'

    # -----------------------------------------------------------------
    def valid_phrase(self) -> str:
        """
        If Pattern is phrase, make sure it's adequately long.
        """

        # Don't check length for non-ascii phrases, since a word
        # in a language like chinese might have len == 1
        if not self.value.isascii():
            return ''

        phrase_len = len(self.value)

        if phrase_len < 5:
            return f'"{self.value}" is too short'

        if 5 <= phrase_len <= 7 and ' ' not in self.value:
            return f'"{self.value}" is too short. Consider prepending or appending a space'

        if chars.search(self.value):
            return f'"{self.value}" - replace punctuation characters with spaces.'

        return ''


# ---------------------------------------------------------------------------
def validate_ip_domain(value: str, action: str = 'raise') -> VALIDATION_ERROR:
    if error := FieldValidator(value).is_ip_domain():
        exc = ValidationError(error, code='invalid')
        if action == 'raise':
            raise exc
        return exc
    return None


# ---------------------------------------------------------------------------
def validate_ip_domain_schema(value: str, action: str = 'raise') -> VALIDATION_ERROR:
    validated_value = FieldValidator(value)
    ip_dom_error = validated_value.is_ip_domain()
    if not ip_dom_error:
        return None

    schema_error = validated_value.valid_url_schema()
    if not schema_error:
        return None

    # allow wild-cards for proxy pac rules
    if '*' in value:
        return None

    error = 'Not a valid IP, Domain or URL schema'

    if action == 'raise':
        raise ValidationError(error, code='invalid')

    return None


# ---------------------------------------------------------------------------
def validate_domain(value: str) -> None:
    """
    Validate that the string is domain or URL
    """
    msg = FieldValidator(value=value).valid_domain()
    if msg:
        raise ValidationError(msg)


# ---------------------------------------------------------------------------
def validate_ip_address(value: str, action: str = 'raise') -> VALIDATION_ERROR:
    if error := FieldValidator(value).valid_ip():
        exc = ValidationError(error, code='invalid')
        if action == 'raise':
            raise exc
        return exc
    return None


# ---------------------------------------------------------------------------
def validate_multiple_words(value: str, action: str = 'raise') -> VALIDATION_ERROR:
    """
    Check for multi-word string
    length for comments field
    """
    if error := FieldValidator(value).is_multiple_words():
        exc = ValidationError(error, code='invalid')
        if action == 'raise':
            raise exc
        return exc
    return None


# ---------------------------------------------------------------------------
def validate_legal_punctuation(
    value: str,
    action: str = 'raise',
    skip: Sequence = (),
) -> VALIDATION_ERROR:
    """
    Check fields for illegal punctuation characters
    """
    if error := FieldValidator(value).legal_punctuation(skip=skip):
        exc = ValidationError(error, code='invalid')
        if action == 'raise':
            raise exc
        return exc
    return None


# ---------------------------------------------------------------------------
def validate_no_punctuation(value: str, action: str = 'raise') -> VALIDATION_ERROR:
    """
    Check fields for any punctuation characters except underscore "_"
    """
    if error := FieldValidator(value).no_punctuation():
        exc = ValidationError(error, code='invalid')
        if action == 'raise':
            raise exc
        return exc
    return None


# ---------------------------------------------------------------------------
def validate_no_spaces(value: str, action: str = 'raise') -> VALIDATION_ERROR:
    """
    Check fields for spaces
    """
    error = FieldValidator(value).no_spaces()
    if error:
        exc = ValidationError(error, code='invalid')
        if action == 'raise':
            raise exc
        return exc
    return None


# ---------------------------------------------------------------------------
def validate_regex(value: str, action: str = 'raise') -> VALIDATION_ERROR:
    """
    If rule is regex, make sure it's properly formed and sufficiently long
    """
    if error := FieldValidator(value).valid_regex():
        exc = ValidationError(error, code='invalid')
        if action == 'raise':
            raise exc
        return exc
    return None


# ---------------------------------------------------------------------------
def validate_domain_url(value: str, action: str = 'raise') -> VALIDATION_ERROR:
    """
    If rule is domain, make sure it's properly formed.
    Proper forms include:

    dell.com
    localhost
    """
    if error := FieldValidator(value).valid_domain():
        exc = ValidationError(error, code='invalid')
        if action == 'raise':
            raise exc
        return exc
    return None


# ---------------------------------------------------------------------------
def validate_phrase(value: str, action: str = 'raise') -> VALIDATION_ERROR:
    """
    If Rule is phrase, make sure it's adequately long.
    """
    if error := FieldValidator(value).valid_phrase():
        exc = ValidationError(error, code='invalid')
        if action == 'raise':
            raise exc
        return exc
    return None


# ---------------------------------------------------------------------------
def validate_username_punctuation(value: str, action: str = 'raise') -> VALIDATION_ERROR:
    """
    Validate usernames with some compatibility with LDAP naming conventions.
    # https://ldapwiki.com/wiki/Best%20Practices%20For%20LDAP%20Naming%20Attributes
    """
    return validate_legal_punctuation(value, action, USERNAME_LEGAL_PUNCTUATION)


# ---------------------------------------------------------------------------
def validate_http_status_code(value: Union[str, int]) -> None:
    """
    Validate that the value is a correct HTTP Status Code
    """
    try:
        value = int(value)
    except (ValueError, TypeError):
        raise ValidationError('Status Code must be an integer') from None

    if value not in settings.STATUS_CODES:
        raise ValidationError(f'{value} is not a valid status code per RFC 2616')


# ---------------------------------------------------------------------------
def validate_http_method(value: str) -> None:
    """
    Validate that the value is a correct HTTP Method
    """
    if value not in settings.HTTP_METHODS:
        raise ValidationError(f'{value} is not a valid HTTP Method')


# ---------------------------------------------------------------------------
def validate_weekdays(days: str) -> None:
    """
    Validate that Weekday abbreviations are limited to the keys
    of the weekdays dictionary

    :param days: Comma-separated string of weekday abbreviations
    """
    abbrevs = weekdays.keys()
    check_days = days.lower().split(',')
    malformed_days = set(check_days).difference(set(abbrevs))

    if malformed_days:
        clean_days = ','.join([d.title() for d in abbrevs])
        malformed = ','.join([d.title() for d in check_days])
        raise ValidationError(
            f'{malformed} is invalid. Day values must be formatted as {clean_days}'
        )


__all__ = (
    'ValidIP',  # convenience scope
    'NoControlCharactersValidator',  # ""
    'ProhibitNullCharactersValidator',  # ""
    'FieldValidator',
    'validate_domain',
    'validate_domain_url',
    'validate_ip_domain',
    'validate_ip_domain_schema',
    'validate_weekdays',
    'validate_http_method',
    'validate_http_status_code',
    'validate_ip_address',
    'validate_multiple_words',
    'validate_legal_punctuation',
    'validate_no_punctuation',
    'validate_username_punctuation',
    'validate_no_spaces',
    'validate_regex',
    'validate_domain_url',
    'validate_phrase',
)
