from ipaddress import ip_address, IPv4Address, IPv6Address, IPv4Network, IPv6Network
import mimetypes
from urllib.parse import urlsplit

from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
from django.utils.translation import gettext_lazy as tr

from console_base.dns import dns_lookup
from console_base.utils import call_once

RANDOM_MAC_BITS = frozenset(('2', '6', 'a', 'e'))
IP_TYPE = str | IPv4Address | IPv6Address | IPv4Network | IPv6Network


@call_once
def mimetype_prefixes() -> set[str]:
    return {mt.split('/')[0] for mt in mimetypes.types_map.values()}


# ----------------------------------------------------------------------
class PortValidator:
    """
    Make sure that the ports and port ranges are valid
    """

    def __init__(self, ports: str) -> None:
        self.ports = ports.split(',')

    # -----------------------------------------------------------------
    def valid_integer(self, port: int | str) -> None:
        try:
            int(port)
        except Exception:
            raise ValidationError(f'{port} is not a valid integer') from None

    # -----------------------------------------------------------------
    def valid_reserved_range(self, port: int | str) -> None:
        port = int(port)
        if 0 >= port or port >= 65536:
            raise ValidationError(f'{port} is not between 1-65536')

    # -----------------------------------------------------------------
    def valid_high_range(self, port: int | str) -> None:
        try:
            int(port)
        except Exception:
            raise ValidationError(f'{port} is not a valid integer') from None

    # -----------------------------------------------------------------
    def valid_port_range(self, port_range: str) -> None:
        """
        Ensure that port range is valid.
        Range should have <low_int>:<high_int> format
        """
        try:
            low, high = port_range.split(':')
        except Exception:
            raise ValidationError(f'{port_range} not a valid Port Range') from None

        if int(low) >= int(high):
            raise ValidationError(f'{low} must be lower than {high}')

        self.valid_integer(low)
        self.valid_integer(high)
        self.valid_reserved_range(low)
        self.valid_reserved_range(high)

    # -----------------------------------------------------------------
    def validate(self) -> None:
        for port in self.ports:
            if ':' in port:
                self.valid_port_range(port)
            else:
                self.valid_integer(port)
                self.valid_reserved_range(port)


# ---------------------------------------------------------------------------
def validate_mac_address(mac: str) -> None:
    """
    Validate MAC / Hardware
    """
    mac = str(mac)  # handle EUI objects
    if mac in ('00:00:00:00:00:00', '00-00-00-00-00-00'):
        raise ValidationError('Do not use gratuitous MAC addresses.')

    if mac.startswith(('00:00:00:00', '00-00-00-00')):
        raise ValidationError('Insufficient data for valid MAC address.')


# ---------------------------------------------------------------------------
def validate_mimetype(value: str) -> None:
    """
    Validate the value is correctly formed mimetype
    """
    try:
        prefix, _ = value.lower().split('/')
    except ValueError:
        raise ValidationError(f'{value} is not a valid Mimetype') from None

    if prefix not in mimetype_prefixes():
        raise ValidationError(f'{value} is not a valid Mimetype')


# ---------------------------------------------------------------------------
def validate_ports(ports: str) -> None:
    """
    Validate that value has valid ports and port ranges

    :param ports: Comma-separated string of port numbers
    """
    PortValidator(ports).validate()


# ---------------------------------------------------------------------------
def validate_private_lan_ip(ip: IP_TYPE) -> None:
    """
    IP is not a self-assigned private address.
    """
    if not ip:
        return

    if isinstance(ip, str):
        try:
            ip = ip_address(ip)
        except ValueError:
            raise ValidationError('Invalid IP address') from None

    validate_routable_ip(ip)
    if not ip.is_private:
        raise ValidationError('Only private LAN addresses are valid for Local Device interfaces')


# ---------------------------------------------------------------------------
def validate_name_or_private_lan_ip(name_or_ip: IP_TYPE) -> None:
    """
    Validate that the input is a valid routable IP or a string name.
    Used to validate fields that might be computer name or ip address.
    """
    # Let model/form field validation handle falsy values
    if not name_or_ip:
        return

    if isinstance(name_or_ip, (IPv4Network, IPv6Network)):
        return validate_private_lan_ip(name_or_ip)

    try:
        ip = ip_address(name_or_ip)
    except ValueError:
        # If it's not an IP at all, let other validation take over.
        return

    validate_private_lan_ip(ip)


# ---------------------------------------------------------------------------
def validate_hyphenated_ip_range(ip_range: str) -> None:
    """
    ip_range must be a correctly formed hyphenated IP range
    """
    if not ip_range:
        return

    ips = [ip.strip() for ip in ip_range.split('-')]
    if len(ips) != 2:
        raise ValidationError('Invalid IP range')

    try:
        ip1 = ip_address(ips[0])
        ip2 = ip_address(ips[1])
    except Exception:
        raise ValidationError('Invalid IP range') from None

    validate_routable_ip(ip1)
    validate_routable_ip(ip2)

    if ip1 > ip2:  # type: ignore
        raise ValidationError('Second IP must be higher than first IP')


# ---------------------------------------------------------------------------
def validate_routable_ip(ip: IP_TYPE) -> None:
    """
    IP Addresses must be routable
    """
    if isinstance(ip, str):
        try:
            ip = ip_address(ip)
        except ValueError:
            raise ValidationError('Invalid IP address') from None

    if ip.is_loopback:
        raise ValidationError('Loop-back IPs are invalid here')
    if ip.is_link_local:
        raise ValidationError('Link local IPs are invalid here')
    if ip.is_multicast:
        raise ValidationError('Multicast IPs are invalid here')
    if ip.is_reserved:
        raise ValidationError('Reserved IPs are invalid here')
    if ip.is_unspecified:
        raise ValidationError('Unspecified IPs are invalid here')


# ---------------------------------------------------------------------------
class RemoteURLValidator(URLValidator):
    """
    Ensure that URL refers to external hostname to avoid
    SSRF (Server-side Request Forgery) attacks.
    """

    local_hostname_msg = tr('URL Hostname must point to remote server location')
    lan_ip_msg = tr('URL IP address must be publicly routable')
    loopback_ip_msg = tr('URL IP address must not be loopback!')

    def __call__(self, value: str) -> None:
        super().__call__(value)

        split_url = urlsplit(value)

        if split_url in ('draw.bridge', 'log.cabin', 'localhost'):
            raise ValidationError(self.local_hostname_msg, self.code, params={'value': value})

        try:
            ips = [ip_address(split_url.netloc)]
            lan_ip_msg, loopback_ip_msg = self.lan_ip_msg, self.loopback_ip_msg
        except ValueError:
            # Do DNS lookup of hostname because user might control DNS server
            # and add custom entry that resolves to the local machine.
            ips = [ip_address(ip) for ip in dns_lookup(split_url.netloc, split_url.port)]
            lan_ip_msg, loopback_ip_msg = self.local_hostname_msg, self.local_hostname_msg

        for ip in ips:
            if ip.is_loopback:
                raise ValidationError(loopback_ip_msg, self.code, params={'value': value})

            if not ip.is_global:
                raise ValidationError(lan_ip_msg, self.code, params={'value': value})


__all__ = (
    'PortValidator',
    'validate_mac_address',
    'validate_mimetype',
    'validate_ports',
    'validate_private_lan_ip',
    'validate_hyphenated_ip_range',
    'validate_name_or_private_lan_ip',
    'validate_routable_ip',
    'RemoteURLValidator',
)
