import json

from django import forms
from django.conf import settings
from django.db.models import BigIntegerField as BigIntegerModelField
from django.db.models import Count
from django.forms.fields import InvalidJSONInput
from django.forms.fields import JSONField as _JSONField
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from netaddr import EUI, AddrFormatError

from utilities.forms import widgets
from utilities.validators import EnhancedURLValidator

__all__ = (
    'BigIntegerField',
    'ColorField',
    'CommentField',
    'JSONField',
    'LaxURLField',
    'MACAddressField',
    'PositiveBigIntegerField',
    'QueryField',
    'SlugField',
    'TagFilterField',
)


class BigIntegerField(forms.IntegerField):
    """
    An IntegerField constrained to the range of a signed 64-bit integer.
    """
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('min_value', -BigIntegerModelField.MAX_BIGINT - 1)
        kwargs.setdefault('max_value', BigIntegerModelField.MAX_BIGINT)
        super().__init__(*args, **kwargs)


class PositiveBigIntegerField(BigIntegerField):
    """
    An IntegerField constrained to the range supported by Django's
    PositiveBigIntegerField model field.
    """
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('min_value', 0)
        super().__init__(*args, **kwargs)


class QueryField(forms.CharField):
    """
    A CharField subclass used for global search/query fields in filter forms.
    This field type signals to FilterModifierMixin to skip enhancement with lookup modifiers.
    """
    pass


class CommentField(forms.CharField):
    """
    A textarea with support for Markdown rendering. Exists mostly just to add a standard `help_text`.
    """
    widget = widgets.MarkdownWidget
    label = _('Comments')
    help_text = _(
        '<i class="mdi mdi-information-outline" aria-hidden="true"></i> '
        '<a href="{url}" target="_blank" tabindex="-1">Markdown</a> syntax is supported'
    ).format(url=static('docs/reference/markdown/'))

    def __init__(self, *, label=label, help_text=help_text, required=False, **kwargs):
        super().__init__(label=label, help_text=help_text, required=required, **kwargs)


class SlugField(forms.SlugField):
    """
    Extend Django's built-in SlugField to automatically populate from a field called `name` unless otherwise specified.

    Parameters:
        slug_source: Name of the form field from which the slug value will be derived
    """
    widget = widgets.SlugWidget
    label = _('Slug')
    help_text = _("URL-friendly unique shorthand")

    def __init__(self, *, slug_source='name', label=label, help_text=help_text, **kwargs):
        super().__init__(label=label, help_text=help_text, **kwargs)

        self.widget.attrs['slug-source'] = slug_source

    def get_bound_field(self, form, field_name):
        if prefix := form.prefix:
            slug_source = self.widget.attrs.get('slug-source')
            if slug_source and not slug_source.startswith(f'{prefix}-'):
                self.widget.attrs['slug-source'] = f"{prefix}-{slug_source}"

        return super().get_bound_field(form, field_name)


class ColorField(forms.CharField):
    """
    A field which represents a color value in hexadecimal `RRGGBB` format. Utilizes NetBox's `ColorSelect` widget to
    render choices.
    """
    widget = widgets.ColorSelect


class TagFilterField(forms.MultipleChoiceField):
    """
    A filter field for the tags of a model. Only the tags used by a model are displayed.

    :param model: The model of the filter
    """

    def __init__(self, model, *args, **kwargs):
        def get_choices():
            tags = model.tags.annotate(
                count=Count('extras_taggeditem_items')
            ).order_by('name')
            return [
                (settings.FILTERS_NULL_CHOICE_VALUE, settings.FILTERS_NULL_CHOICE_LABEL),  # "None" option
                *[(str(tag.slug), f'{tag.name} ({tag.count})') for tag in tags]
            ]

        # Choices are fetched each time the form is initialized
        super().__init__(label=_('Tags'), choices=get_choices, required=False, *args, **kwargs)


class LaxURLField(forms.URLField):
    """
    Modifies Django's built-in URLField to remove the requirement for fully-qualified domain names
    (e.g. http://myserver/ is valid)
    """
    default_validators = [EnhancedURLValidator()]


class JSONField(_JSONField):
    """
    Custom wrapper around Django's built-in JSONField to avoid presenting "null" as the default text.
    """
    empty_values = [None, '', ()]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.widget.attrs['placeholder'] = ''
        self.widget.attrs['class'] = 'font-monospace'
        if not self.help_text:
            self.help_text = _('Enter context data in <a href="https://json.org/">JSON</a> format.')

    def prepare_value(self, value):
        if isinstance(value, InvalidJSONInput):
            return value
        if value in ('', None):
            return ''
        if type(value) is str:
            try:
                value = json.loads(value, cls=self.decoder)
            except json.decoder.JSONDecodeError:
                return f'"{value}"'
        return json.dumps(value, sort_keys=True, indent=4, ensure_ascii=False, cls=self.encoder)


class MACAddressField(forms.Field):
    """
    Validates a 48-bit MAC address.
    """
    widget = forms.CharField
    default_error_messages = {
        'invalid': _('MAC address must be in EUI-48 format'),
    }

    def to_python(self, value):
        value = super().to_python(value)

        # Validate MAC address format
        try:
            value = EUI(value.strip())
        except AddrFormatError:
            raise forms.ValidationError(self.error_messages['invalid'], code='invalid')

        return value
