import json
import re

from django import forms
from django.contrib.postgres.forms import SimpleArrayField
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _

from core.forms.mixins import SyncedDataMixin
from core.models import ObjectType
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.choices import *
from extras.constants import IMAGE_ATTACHMENT_IMAGE_FORMATS
from extras.models import *
from netbox.events import get_event_type_choices
from netbox.forms import NetBoxModelForm, PrimaryModelForm
from netbox.forms.mixins import ChangelogMessageMixin, OwnerMixin
from tenancy.models import Tenant, TenantGroup
from users.models import Group, User
from utilities.forms import get_field_value
from utilities.forms.fields import (
    CommentField,
    ContentTypeChoiceField,
    ContentTypeMultipleChoiceField,
    DynamicModelChoiceField,
    DynamicModelMultipleChoiceField,
    JSONField,
    SlugField,
)
from utilities.forms.rendering import FieldSet, ObjectAttribute
from utilities.forms.widgets import ChoicesWidget, HTMXSelect
from utilities.tables import get_table_for_model
from virtualization.models import Cluster, ClusterGroup, ClusterType

__all__ = (
    'BookmarkForm',
    'ConfigContextForm',
    'ConfigContextProfileForm',
    'ConfigTemplateForm',
    'CustomFieldChoiceSetForm',
    'CustomFieldForm',
    'CustomLinkForm',
    'EventRuleForm',
    'ExportTemplateForm',
    'ImageAttachmentForm',
    'JournalEntryForm',
    'NotificationGroupForm',
    'SavedFilterForm',
    'SubscriptionForm',
    'TableConfigForm',
    'TagForm',
    'WebhookForm',
)


class CustomFieldForm(ChangelogMessageMixin, OwnerMixin, forms.ModelForm):
    object_types = ContentTypeMultipleChoiceField(
        label=_('Object types'),
        queryset=ObjectType.objects.with_feature('custom_fields'),
        help_text=_("The type(s) of object that have this custom field")
    )
    default = JSONField(
        label=_('Default value'),
        required=False
    )
    related_object_type = ContentTypeChoiceField(
        label=_('Related object type'),
        queryset=ObjectType.objects.public(),
        help_text=_("Type of the related object (for object/multi-object fields only)")
    )
    related_object_filter = JSONField(
        label=_('Related object filter'),
        required=False,
        help_text=_('Specify query parameters as a JSON object.')
    )
    choice_set = DynamicModelChoiceField(
        queryset=CustomFieldChoiceSet.objects.all()
    )
    validation_schema = JSONField(
        label=_('Validation schema'),
        required=False,
        help_text=_('A JSON schema definition for validating the custom field value')
    )
    comments = CommentField()

    fieldsets = (
        FieldSet(
            'object_types', 'name', 'label', 'group_name', 'description', 'type', 'required', 'unique', 'default',
            name=_('Custom Field')
        ),
        FieldSet(
            'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable', name=_('Behavior')
        ),
    )

    class Meta:
        model = CustomField
        fields = '__all__'
        help_texts = {
            'type': _(
                "The type of data stored in this field. For object/multi-object fields, select the related object "
                "type below."
            ),
            'description': _("This will be displayed as help text for the form field. Markdown is supported.")
        }

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

        # Mimic HTMXSelect()
        self.fields['type'].widget.attrs.update({
            'hx-get': '.',
            'hx-include': '#form_fields',
            'hx-target': '#form_fields',
        })

        # Disable changing the type of a CustomField as it almost universally causes errors if custom field data
        # is already present.
        if self.instance.pk:
            self.fields['type'].disabled = True

        field_type = get_field_value(self, 'type')

        # Adjust for text fields
        if field_type in (
                CustomFieldTypeChoices.TYPE_TEXT,
                CustomFieldTypeChoices.TYPE_LONGTEXT,
                CustomFieldTypeChoices.TYPE_URL
        ):
            self.fieldsets = (
                self.fieldsets[0],
                FieldSet('validation_regex', name=_('Validation')),
                *self.fieldsets[1:]
            )
        else:
            del self.fields['validation_regex']

        # Adjust for numeric fields
        if field_type in (
                CustomFieldTypeChoices.TYPE_INTEGER,
                CustomFieldTypeChoices.TYPE_DECIMAL
        ):
            self.fieldsets = (
                self.fieldsets[0],
                FieldSet('validation_minimum', 'validation_maximum', name=_('Validation')),
                *self.fieldsets[1:]
            )
        else:
            del self.fields['validation_minimum']
            del self.fields['validation_maximum']

        # Adjust for JSON fields
        if field_type == CustomFieldTypeChoices.TYPE_JSON:
            self.fieldsets = (
                self.fieldsets[0],
                FieldSet('validation_schema', name=_('Validation')),
                *self.fieldsets[1:]
            )
        else:
            del self.fields['validation_schema']

        # Adjust for object & multi-object fields
        if field_type in (
                CustomFieldTypeChoices.TYPE_OBJECT,
                CustomFieldTypeChoices.TYPE_MULTIOBJECT
        ):
            self.fieldsets = (
                self.fieldsets[0],
                FieldSet('related_object_type', 'related_object_filter', name=_('Related Object')),
                *self.fieldsets[1:]
            )
        else:
            del self.fields['related_object_type']
            del self.fields['related_object_filter']

        # Adjust for selection & multi-select fields
        if field_type in (
                CustomFieldTypeChoices.TYPE_SELECT,
                CustomFieldTypeChoices.TYPE_MULTISELECT
        ):
            self.fieldsets = (
                self.fieldsets[0],
                FieldSet('choice_set', name=_('Choices')),
                *self.fieldsets[1:]
            )
        else:
            del self.fields['choice_set']


class CustomFieldChoiceSetForm(ChangelogMessageMixin, OwnerMixin, forms.ModelForm):
    # TODO: The extra_choices field definition diverge from the CustomFieldChoiceSet model
    extra_choices = forms.CharField(
        widget=ChoicesWidget(),
        required=False,
        help_text=mark_safe(_(
            'Enter one choice per line. An optional label may be specified for each choice by appending it with a '
            'colon. Example:'
        ) + ' <code>choice1:First Choice</code>')
    )
    choice_colors = forms.CharField(
        widget=ChoicesWidget(),
        required=False,
        help_text=mark_safe(
            _(
                'Bind an optional color to a choice by its value. Enter one mapping per line as '
                '<code>value:color</code>. Example:'
            )
            + ' <code>choice1:red</code><br />'
            + _('Supported colors: {colors}').format(
                colors=', '.join(f'<code>{color}</code>' for color in CustomFieldChoiceColorChoices.values())
            )
        ),
    )

    fieldsets = (
        FieldSet(
            'name', 'description', 'base_choices', 'extra_choices', 'choice_colors', 'order_alphabetically',
            name=_('Custom Field Choice Set')
        ),
    )

    class Meta:
        model = CustomFieldChoiceSet
        fields = (
            'name', 'description', 'base_choices', 'extra_choices', 'choice_colors', 'order_alphabetically', 'owner'
        )

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

        # TODO: The check for str / list below is to handle difference in extra_choices field definition
        # In CustomFieldChoiceSetForm, extra_choices is a CharField but in CustomFieldChoiceSet, it is an ArrayField
        # if standardize these, we can simplify this code

        # Convert extra_choices Array Field from model to CharField for form
        if extra_choices := self.initial.get('extra_choices', None):
            if isinstance(extra_choices, str):
                extra_choices = [extra_choices]
            choices = []
            for choice in extra_choices:
                # Setup choices in Add Another use case
                if isinstance(choice, str):
                    choice_str = ":".join(choice.replace("'", "").replace(" ", "")[1:-1].split(","))
                    choices.append(choice_str)
                # Setup choices in Edit use case
                elif isinstance(choice, list):
                    value = choice[0].replace(':', '\\:')
                    label = choice[1].replace(':', '\\:')
                    choices.append(f'{value}:{label}')

            self.initial['extra_choices'] = '\n'.join(choices)

        # Convert choice_colors JSONField from model to CharField for form
        if 'choice_colors' in self.initial:
            choice_colors = self.initial.get('choice_colors') or {}

            if isinstance(choice_colors, str):
                choice_colors = json.loads(choice_colors)

            mappings = []
            for value, color in sorted(choice_colors.items()):
                value = value.replace(':', '\\:')
                mappings.append(f'{value}:{color}')

            self.initial['choice_colors'] = '\n'.join(mappings)

    def clean_extra_choices(self):
        data = []
        for line in self.cleaned_data['extra_choices'].splitlines():
            if not line.strip():
                continue
            try:
                value, label = re.split(r'(?<!\\):', line, maxsplit=1)
                value = value.replace('\\:', ':')
                label = label.replace('\\:', ':')
            except ValueError:
                value, label = line, line
            data.append((value.strip(), label.strip()))
        return data

    def clean_choice_colors(self):
        data = {}
        for line in self.cleaned_data['choice_colors'].splitlines():
            if not line.strip():
                continue
            try:
                value, color = re.split(r'(?<!\\):', line, maxsplit=1)
                value = value.replace('\\:', ':')
            except ValueError as e:
                raise forms.ValidationError(
                    _("Invalid color mapping '{line}'. Use the format value:color.").format(line=line)
                ) from e

            value = value.strip()
            color = color.strip()
            if value in data:
                raise forms.ValidationError(
                    _("Duplicate color mapping defined for choice '{value}'.").format(value=value)
                )
            data[value] = color
        return data


class CustomLinkForm(ChangelogMessageMixin, OwnerMixin, forms.ModelForm):
    object_types = ContentTypeMultipleChoiceField(
        label=_('Object types'),
        queryset=ObjectType.objects.with_feature('custom_links')
    )

    fieldsets = (
        FieldSet(
            'name', 'object_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window',
            name=_('Custom Link')
        ),
        FieldSet('link_text', 'link_url', name=_('Templates')),
    )

    class Meta:
        model = CustomLink
        fields = '__all__'
        widgets = {
            'link_text': forms.Textarea(attrs={'class': 'font-monospace'}),
            'link_url': forms.Textarea(attrs={'class': 'font-monospace'}),
        }
        help_texts = {
            'link_text': _(
                "Jinja2 template code for the link text. Reference the object as {example}. Links "
                "which render as empty text will not be displayed."
            ).format(example="<code>{{ object }}</code>"),
            'link_url': _(
                "Jinja2 template code for the link URL. Reference the object as {example}."
            ).format(example="<code>{{ object }}</code>"),
        }


class ExportTemplateForm(ChangelogMessageMixin, SyncedDataMixin, OwnerMixin, forms.ModelForm):
    object_types = ContentTypeMultipleChoiceField(
        label=_('Object types'),
        queryset=ObjectType.objects.with_feature('export_templates')
    )
    template_code = forms.CharField(
        label=_('Template code'),
        required=False,
        widget=forms.Textarea(attrs={'class': 'font-monospace'})
    )

    fieldsets = (
        FieldSet('name', 'object_types', 'description', 'template_code', name=_('Export Template')),
        FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
        FieldSet(
            'mime_type', 'file_name', 'file_extension', 'environment_params', 'as_attachment', name=_('Rendering')
        ),
    )

    class Meta:
        model = ExportTemplate
        fields = '__all__'

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

        # Disable data field when a DataFile has been set
        if self.instance.data_file:
            self.fields['template_code'].widget.attrs['readonly'] = True
            self.fields['template_code'].help_text = _(
                'Template content is populated from the remote source selected below.'
            )

    def clean(self):
        super().clean()

        if not self.cleaned_data.get('template_code') and not self.cleaned_data.get('data_file'):
            raise forms.ValidationError(_("Must specify either local content or a data file"))

        return self.cleaned_data


class SavedFilterForm(ChangelogMessageMixin, OwnerMixin, forms.ModelForm):
    slug = SlugField()
    object_types = ContentTypeMultipleChoiceField(
        label=_('Object types'),
        queryset=ObjectType.objects.all()
    )
    parameters = JSONField()

    fieldsets = (
        FieldSet('name', 'slug', 'object_types', 'description', 'weight', 'enabled', 'shared', name=_('Saved Filter')),
        FieldSet('parameters', name=_('Parameters')),
    )

    class Meta:
        model = SavedFilter
        exclude = ('user',)

    def __init__(self, *args, initial=None, **kwargs):

        # Convert any parameters delivered via initial data to JSON data
        if initial and 'parameters' in initial:
            if type(initial['parameters']) is str:
                initial['parameters'] = json.loads(initial['parameters'])

        super().__init__(*args, initial=initial, **kwargs)


class TableConfigForm(ChangelogMessageMixin, forms.ModelForm):
    object_type = ContentTypeChoiceField(
        label=_('Object type'),
        queryset=ObjectType.objects.all()
    )
    ordering = SimpleArrayField(
        base_field=forms.CharField(),
        required=False,
        label=_('Ordering'),
        help_text=_(
            "Enter a comma-separated list of column names. Prepend a name with a hyphen to reverse the order."
        )
    )
    available_columns = SimpleArrayField(
        base_field=forms.CharField(),
        required=False,
        widget=forms.SelectMultiple(
            attrs={'size': 10, 'class': 'form-select'}
        ),
        label=_('Available Columns')
    )
    columns = SimpleArrayField(
        base_field=forms.CharField(),
        widget=forms.SelectMultiple(
            attrs={'size': 10, 'class': 'form-select select-all'}
        ),
        label=_('Selected Columns')
    )

    class Meta:
        model = TableConfig
        exclude = ('user',)

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

        self.fields['available_columns'].widget.choices = ()
        self.fields['columns'].widget.choices = ()

        # Table context may be absent e.g. when the add view is requested directly
        object_type_pk = get_field_value(self, 'object_type')
        object_type_pk = getattr(object_type_pk, 'pk', object_type_pk)
        if not object_type_pk:
            return

        try:
            object_type = ObjectType.objects.get(pk=object_type_pk)
        except (ObjectType.DoesNotExist, TypeError, ValueError):
            return

        model = object_type.model_class()
        if model is None:
            return

        table_name = get_field_value(self, 'table')
        table_class = get_table_for_model(model, table_name)
        if table_class is None:
            return

        table = table_class([])

        if columns := self._get_columns():
            table._set_columns(columns)

        # Initialize columns field based on table attributes
        self.fields['available_columns'].widget.choices = table.available_columns
        self.fields['columns'].widget.choices = table.selected_columns

    def _get_columns(self):
        if self.is_bound and (columns := self.data.getlist('columns')):
            return columns
        if 'columns' in self.initial:
            columns = self.get_initial_for_field(self.fields['columns'], 'columns')
            return columns.split(',') if type(columns) is str else columns
        if self.instance is not None:
            return self.instance.columns
        return None


class BookmarkForm(forms.ModelForm):
    object_type = ContentTypeChoiceField(
        label=_('Object type'),
        queryset=ObjectType.objects.with_feature('bookmarks')
    )

    class Meta:
        model = Bookmark
        fields = ('object_type', 'object_id')


class NotificationGroupForm(ChangelogMessageMixin, forms.ModelForm):
    groups = DynamicModelMultipleChoiceField(
        label=_('Groups'),
        required=False,
        queryset=Group.objects.all()
    )
    users = DynamicModelMultipleChoiceField(
        label=_('Users'),
        required=False,
        queryset=User.objects.all()
    )

    class Meta:
        model = NotificationGroup
        fields = ('name', 'description', 'groups', 'users')

    def clean(self):
        super().clean()

        # At least one User or Group must be assigned
        if not self.cleaned_data['groups'] and not self.cleaned_data['users']:
            raise forms.ValidationError(_("A notification group specify at least one user or group."))

        return self.cleaned_data


class SubscriptionForm(forms.ModelForm):
    object_type = ContentTypeChoiceField(
        label=_('Object type'),
        queryset=ObjectType.objects.with_feature('notifications')
    )

    class Meta:
        model = Subscription
        fields = ('object_type', 'object_id')


class WebhookForm(OwnerMixin, NetBoxModelForm):

    fieldsets = (
        FieldSet('name', 'description', 'tags', name=_('Webhook')),
        FieldSet(
            'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
            name=_('HTTP Request')
        ),
        FieldSet('ssl_verification', 'ca_file_path', name=_('SSL')),
    )

    class Meta:
        model = Webhook
        fields = '__all__'
        widgets = {
            'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
            'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
        }


class EventRuleForm(OwnerMixin, NetBoxModelForm):
    object_types = ContentTypeMultipleChoiceField(
        label=_('Object types'),
        queryset=ObjectType.objects.with_feature('event_rules'),
    )
    event_types = forms.MultipleChoiceField(
        choices=get_event_type_choices(),
        label=_('Event types')
    )
    action_choice = forms.ChoiceField(
        label=_('Action choice'),
        choices=[]
    )
    conditions = JSONField(
        required=False,
        help_text=_('Enter conditions in <a href="https://json.org/">JSON</a> format.')
    )
    action_data = JSONField(
        required=False,
        help_text=_('Enter parameters to pass to the action in <a href="https://json.org/">JSON</a> format.')
    )
    comments = CommentField()

    fieldsets = (
        FieldSet('name', 'description', 'object_types', 'enabled', 'tags', name=_('Event Rule')),
        FieldSet('event_types', 'conditions', name=_('Triggers')),
        FieldSet('action_type', 'action_choice', 'action_data', name=_('Action')),
    )

    class Meta:
        model = EventRule
        fields = (
            'object_types', 'name', 'description', 'enabled', 'event_types', 'conditions', 'action_type',
            'action_object_type', 'action_object_id', 'action_data', 'owner', 'comments', 'tags'
        )
        widgets = {
            'conditions': forms.Textarea(attrs={'class': 'font-monospace'}),
            'action_type': HTMXSelect(),
            'action_object_type': forms.HiddenInput,
            'action_object_id': forms.HiddenInput,
        }

    def init_script_choice(self):
        initial = None
        if self.instance.action_type == EventRuleActionChoices.SCRIPT:
            script_id = get_field_value(self, 'action_object_id')
            initial = Script.objects.get(pk=script_id) if script_id else None
        self.fields['action_choice'] = DynamicModelChoiceField(
            label=_('Script'),
            queryset=Script.objects.all(),
            required=True,
            initial=initial
        )

    def init_webhook_choice(self):
        initial = None
        if self.instance.action_type == EventRuleActionChoices.WEBHOOK:
            webhook_id = get_field_value(self, 'action_object_id')
            initial = Webhook.objects.get(pk=webhook_id) if webhook_id else None
        self.fields['action_choice'] = DynamicModelChoiceField(
            label=_('Webhook'),
            queryset=Webhook.objects.all(),
            required=True,
            initial=initial
        )

    def init_notificationgroup_choice(self):
        initial = None
        if self.instance.action_type == EventRuleActionChoices.NOTIFICATION:
            notificationgroup_id = get_field_value(self, 'action_object_id')
            initial = NotificationGroup.objects.get(pk=notificationgroup_id) if notificationgroup_id else None
        self.fields['action_choice'] = DynamicModelChoiceField(
            label=_('Notification group'),
            queryset=NotificationGroup.objects.all(),
            required=True,
            initial=initial
        )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['action_object_type'].required = False
        self.fields['action_object_id'].required = False

        # Determine the action type
        action_type = get_field_value(self, 'action_type')

        if action_type == EventRuleActionChoices.WEBHOOK:
            self.init_webhook_choice()
        elif action_type == EventRuleActionChoices.SCRIPT:
            self.init_script_choice()
        elif action_type == EventRuleActionChoices.NOTIFICATION:
            self.init_notificationgroup_choice()

    def clean(self):
        super().clean()

        action_choice = self.cleaned_data.get('action_choice')
        # Webhook
        if self.cleaned_data.get('action_type') == EventRuleActionChoices.WEBHOOK:
            self.cleaned_data['action_object_type'] = ObjectType.objects.get_for_model(action_choice)
            self.cleaned_data['action_object_id'] = action_choice.id
        # Script
        elif self.cleaned_data.get('action_type') == EventRuleActionChoices.SCRIPT:
            self.cleaned_data['action_object_type'] = ObjectType.objects.get_for_model(
                Script,
                for_concrete_model=False
            )
            self.cleaned_data['action_object_id'] = action_choice.id
        # Notification
        elif self.cleaned_data.get('action_type') == EventRuleActionChoices.NOTIFICATION:
            self.cleaned_data['action_object_type'] = ObjectType.objects.get_for_model(action_choice)
            self.cleaned_data['action_object_id'] = action_choice.id

        return self.cleaned_data


class TagForm(ChangelogMessageMixin, OwnerMixin, forms.ModelForm):
    slug = SlugField()
    object_types = ContentTypeMultipleChoiceField(
        label=_('Object types'),
        queryset=ObjectType.objects.with_feature('tags'),
        required=False
    )

    fieldsets = (
        FieldSet('name', 'slug', 'color', 'weight', 'description', 'object_types', name=_('Tag')),
    )

    class Meta:
        model = Tag
        fields = [
            'name', 'slug', 'color', 'weight', 'description', 'object_types', 'owner',
        ]


class ConfigContextProfileForm(SyncedDataMixin, PrimaryModelForm):
    schema = JSONField(
        label=_('Schema'),
        required=False,
        help_text=_("Enter a valid JSON schema to define supported attributes.")
    )
    tags = DynamicModelMultipleChoiceField(
        label=_('Tags'),
        queryset=Tag.objects.all(),
        required=False
    )

    fieldsets = (
        FieldSet('name', 'description', 'schema', 'tags', name=_('Config Context Profile')),
        FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
    )

    class Meta:
        model = ConfigContextProfile
        fields = (
            'name', 'description', 'schema', 'data_source', 'data_file', 'auto_sync_enabled', 'owner', 'comments',
            'tags',
        )


class ConfigContextForm(ChangelogMessageMixin, SyncedDataMixin, OwnerMixin, forms.ModelForm):
    profile = DynamicModelChoiceField(
        label=_('Profile'),
        queryset=ConfigContextProfile.objects.all(),
        required=False
    )
    regions = DynamicModelMultipleChoiceField(
        label=_('Regions'),
        queryset=Region.objects.all(),
        required=False
    )
    site_groups = DynamicModelMultipleChoiceField(
        label=_('Site groups'),
        queryset=SiteGroup.objects.all(),
        required=False
    )
    sites = DynamicModelMultipleChoiceField(
        label=_('Sites'),
        queryset=Site.objects.all(),
        required=False
    )
    locations = DynamicModelMultipleChoiceField(
        label=_('Locations'),
        queryset=Location.objects.all(),
        required=False
    )
    device_types = DynamicModelMultipleChoiceField(
        label=_('Device types'),
        queryset=DeviceType.objects.all(),
        required=False
    )
    roles = DynamicModelMultipleChoiceField(
        label=_('Roles'),
        queryset=DeviceRole.objects.all(),
        required=False
    )
    platforms = DynamicModelMultipleChoiceField(
        label=_('Platforms'),
        queryset=Platform.objects.all(),
        required=False
    )
    cluster_types = DynamicModelMultipleChoiceField(
        label=_('Cluster types'),
        queryset=ClusterType.objects.all(),
        required=False
    )
    cluster_groups = DynamicModelMultipleChoiceField(
        label=_('Cluster groups'),
        queryset=ClusterGroup.objects.all(),
        required=False
    )
    clusters = DynamicModelMultipleChoiceField(
        label=_('Clusters'),
        queryset=Cluster.objects.all(),
        required=False
    )
    tenant_groups = DynamicModelMultipleChoiceField(
        label=_('Tenant groups'),
        queryset=TenantGroup.objects.all(),
        required=False
    )
    tenants = DynamicModelMultipleChoiceField(
        label=_('Tenants'),
        queryset=Tenant.objects.all(),
        required=False
    )
    tags = DynamicModelMultipleChoiceField(
        label=_('Tags'),
        queryset=Tag.objects.all(),
        required=False
    )
    data = JSONField(
        label=_('Data'),
        required=False
    )

    fieldsets = (
        FieldSet('name', 'weight', 'profile', 'description', 'data', 'is_active', name=_('Config Context')),
        FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
        FieldSet(
            'regions', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types',
            'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags',
            name=_('Assignment')
        ),
    )

    class Meta:
        model = ConfigContext
        fields = (
            'name', 'weight', 'profile', 'description', 'data', 'is_active', 'regions', 'site_groups', 'sites',
            'locations', 'roles', 'device_types', 'platforms', 'cluster_types', 'cluster_groups', 'clusters',
            'tenant_groups', 'tenants', 'owner', 'tags', 'data_source', 'data_file', 'auto_sync_enabled',
        )

    def __init__(self, *args, initial=None, **kwargs):

        # Convert data delivered via initial data to JSON data
        if initial and 'data' in initial:
            if type(initial['data']) is str:
                initial['data'] = json.loads(initial['data'])

        super().__init__(*args, initial=initial, **kwargs)

        # Disable data field when a DataFile has been set
        if self.instance.data_file:
            self.fields['data'].widget.attrs['readonly'] = True
            self.fields['data'].help_text = _('Data is populated from the remote source selected below.')

    def clean(self):
        super().clean()

        if not self.cleaned_data.get('data') and not self.cleaned_data.get('data_file'):
            raise forms.ValidationError(_("Must specify either local data or a data file"))

        return self.cleaned_data


class ConfigTemplateForm(ChangelogMessageMixin, SyncedDataMixin, OwnerMixin, forms.ModelForm):
    tags = DynamicModelMultipleChoiceField(
        label=_('Tags'),
        queryset=Tag.objects.all(),
        required=False
    )
    template_code = forms.CharField(
        label=_('Template code'),
        required=False,
        widget=forms.Textarea(attrs={'class': 'font-monospace'})
    )

    fieldsets = (
        FieldSet('name', 'description', 'tags', 'template_code', name=_('Config Template')),
        FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
        FieldSet(
            'mime_type', 'file_name', 'file_extension', 'environment_params', 'as_attachment', 'debug',
            name=_('Rendering')
        ),
    )

    class Meta:
        model = ConfigTemplate
        fields = '__all__'
        widgets = {
            'environment_params': forms.Textarea(attrs={'rows': 5})
        }

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

        # Disable content field when a DataFile has been set
        if self.instance.data_file:
            self.fields['template_code'].widget.attrs['readonly'] = True
            self.fields['template_code'].help_text = _(
                'Template content is populated from the remote source selected below.'
            )

    def clean(self):
        super().clean()

        if not self.cleaned_data.get('template_code') and not self.cleaned_data.get('data_file'):
            raise forms.ValidationError(_("Must specify either local content or a data file"))

        return self.cleaned_data


class ImageAttachmentForm(forms.ModelForm):
    fieldsets = (
        FieldSet(ObjectAttribute('parent'), 'image', 'name', 'description'),
    )

    class Meta:
        model = ImageAttachment
        fields = [
            'image', 'name', 'description',
        ]
        # Explicitly set 'image/avif' to support AVIF selection in Firefox
        widgets = {
            'image': forms.ClearableFileInput(
                attrs={'accept': ','.join(sorted(set(IMAGE_ATTACHMENT_IMAGE_FORMATS.values())))}
            ),
        }


class JournalEntryForm(NetBoxModelForm):
    kind = forms.ChoiceField(
        label=_('Kind'),
        choices=JournalEntryKindChoices
    )
    comments = CommentField(required=True)

    class Meta:
        model = JournalEntry
        fields = ['assigned_object_type', 'assigned_object_id', 'kind', 'tags', 'comments']
        widgets = {
            'assigned_object_type': forms.HiddenInput,
            'assigned_object_id': forms.HiddenInput,
        }
