from typing import Optional
from typing import Union

from .exceptions_types import EmailSyntaxError
from .exceptions_types import ValidatedEmail
from .rfc_constants import CASE_INSENSITIVE_MAILBOX_NAMES
from .syntax import split_email
from .syntax import validate_email_domain_literal
from .syntax import validate_email_domain_name
from .syntax import validate_email_length
from .syntax import validate_email_local_part


def validate_email(
    email: Union[str, bytes],
    /,  # prior arguments are positional-only
    *,  # subsequent arguments are keyword-only
    allow_smtputf8: Optional[bool] = None,
    allow_empty_local: bool = False,
    allow_quoted_local: Optional[bool] = None,
    allow_domain_literal: Optional[bool] = None,
    check_deliverability: Optional[bool] = None,
    test_environment: Optional[bool] = None,
    globally_deliverable: Optional[bool] = None,
    timeout: Optional[int] = None,
    dns_resolver: Optional[object] = None,
) -> ValidatedEmail:
    """
    Given an email address, and some options, returns a ValidatedEmail instance
    with information about the address if it is valid or, if the address is not
    valid, raises an EmailNotValidError. This is the main function of the module.
    """

    # Fill in default values of arguments.
    from . import ALLOW_DOMAIN_LITERAL
    from . import ALLOW_QUOTED_LOCAL
    from . import ALLOW_SMTPUTF8
    from . import CHECK_DELIVERABILITY
    from . import DEFAULT_TIMEOUT
    from . import GLOBALLY_DELIVERABLE
    from . import TEST_ENVIRONMENT

    if allow_smtputf8 is None:
        allow_smtputf8 = ALLOW_SMTPUTF8
    if allow_quoted_local is None:
        allow_quoted_local = ALLOW_QUOTED_LOCAL
    if allow_domain_literal is None:
        allow_domain_literal = ALLOW_DOMAIN_LITERAL
    if check_deliverability is None:
        check_deliverability = CHECK_DELIVERABILITY
    if test_environment is None:
        test_environment = TEST_ENVIRONMENT
    if globally_deliverable is None:
        globally_deliverable = GLOBALLY_DELIVERABLE
    if timeout is None and dns_resolver is None:
        timeout = DEFAULT_TIMEOUT

    # Allow email to be a str or bytes instance. If bytes,
    # it must be ASCII because that's how the bytes work
    # on the wire with SMTP.
    if not isinstance(email, str):
        try:
            email = email.decode('ascii')
        except ValueError as e:
            raise EmailSyntaxError('The email address is not valid ASCII.') from e

    # Split the address into the local part (before the @-sign)
    # and the domain part (after the @-sign). Normally, there
    # is only one @-sign. But the awkward "quoted string" local
    # part form (RFC 5321 4.1.2) allows @-signs in the local
    # part if the local part is quoted.
    local_part, domain_part, is_quoted_local_part = split_email(email)

    # Collect return values in this instance.
    ret = ValidatedEmail()
    ret.original = email

    # Validate the email address's local part syntax and get a normalized form.
    # If the original address was quoted and the decoded local part is a valid
    # unquoted local part, then we'll get back a normalized (unescaped) local
    # part.
    local_part_info = validate_email_local_part(
        local_part,
        allow_smtputf8=allow_smtputf8,
        allow_empty_local=allow_empty_local,
        quoted_local_part=is_quoted_local_part,
    )
    ret.local_part = local_part_info['local_part']
    ret.ascii_local_part = local_part_info['ascii_local_part']
    ret.smtputf8 = local_part_info['smtputf8']

    # If a quoted local part isn't allowed but is present, now raise an exception.
    # This is done after any exceptions raised by validate_email_local_part so
    # that mandatory checks have highest precedence.
    if is_quoted_local_part and not allow_quoted_local:
        raise EmailSyntaxError('Quoting the part before the @-sign is not allowed here.')

    # Some local parts are required to be case-insensitive, so we should normalize
    # to lowercase.
    # RFC 2142
    if (
        ret.ascii_local_part is not None
        and ret.ascii_local_part.lower() in CASE_INSENSITIVE_MAILBOX_NAMES
        and ret.local_part is not None
    ):
        ret.ascii_local_part = ret.ascii_local_part.lower()
        ret.local_part = ret.local_part.lower()

    # Validate the email address's domain part syntax and get a normalized form.
    is_domain_literal = False
    if len(domain_part) == 0:
        raise EmailSyntaxError('There must be something after the @-sign.')

    elif domain_part.startswith('[') and domain_part.endswith(']'):
        # Parse the address in the domain literal and get back a normalized domain.
        domain_part_info = validate_email_domain_literal(domain_part[1:-1])
        if not allow_domain_literal:
            raise EmailSyntaxError(
                'A bracketed IP address after the @-sign is not allowed here.',
            )
        ret.domain = domain_part_info['domain']
        ret.ascii_domain = domain_part_info['domain']  # Domain literals are always ASCII.
        ret.domain_address = domain_part_info['domain_address']
        is_domain_literal = True  # Prevent deliverability checks.

    else:
        # Check the syntax of the domain and get back a normalized
        # internationalized and ASCII form.
        domain_part_info = validate_email_domain_name(
            domain_part,
            test_environment=test_environment,
            globally_deliverable=globally_deliverable,
        )
        ret.domain = domain_part_info['domain']
        ret.ascii_domain = domain_part_info['ascii_domain']

    # Construct the complete normalized form.
    ret.normalized = ret.local_part + '@' + ret.domain

    # If the email address has an ASCII form, add it.
    if not ret.smtputf8:
        if not ret.ascii_domain:
            raise Exception('Missing ASCII domain.')
        ret.ascii_email = (ret.ascii_local_part or '') + '@' + ret.ascii_domain
    else:
        ret.ascii_email = None

    # Check the length of the address.
    validate_email_length(ret)

    if check_deliverability and not test_environment:
        # Validate the email address's deliverability using DNS
        # and update the returned ValidatedEmail object with metadata.

        if is_domain_literal:
            # There is nothing to check --- skip deliverability checks.
            return ret

        # Lazy load `deliverability` as it is slow to import (due to dns.resolver)
        from .deliverability import validate_email_deliverability

        deliverability_info = validate_email_deliverability(
            ret.ascii_domain,
            ret.domain,
            timeout,
            dns_resolver,
        )
        for key, value in deliverability_info.items():
            setattr(ret, key, value)

    return ret
