from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.validators import EMPTY_VALUES
from django.utils.translation import gettext_lazy as _
from timezone_field import TimeZoneFormField

from dcim.choices import *
from dcim.constants import *
from dcim.forms.mixins import FrontPortFormMixin
from dcim.models import *
from extras.models import ConfigTemplate
from ipam.choices import VLANQinQRoleChoices
from ipam.models import ASN, VLAN, VRF, IPAddress, VLANGroup, VLANTranslationPolicy
from netbox.choices import WeightUnitChoices
from netbox.forms import NestedGroupModelForm, NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm
from netbox.forms.mixins import ChangelogMessageMixin, OwnerMixin
from tenancy.forms import TenancyForm
from users.models import User
from utilities.choices import Choice
from utilities.forms import add_blank_choice, get_field_value
from utilities.forms.fields import (
    ChoiceField,
    DynamicModelChoiceField,
    DynamicModelMultipleChoiceField,
    JSONField,
    NumericArrayField,
    SlugField,
    TypedChoiceField,
)
from utilities.forms.rendering import FieldSet, InlineFields, M2MAddRemoveFields, TabbedGroups
from utilities.forms.widgets import (
    APISelect,
    ClearableFileInput,
    ClearableSelect,
    DatePicker,
    HTMXSelect,
    NumberWithOptions,
    SelectWithPK,
)
from utilities.jsonschema import JSONSchemaProperty
from virtualization.models import Cluster, VMInterface
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
from wireless.models import WirelessLAN, WirelessLANGroup

from .common import InterfaceCommonForm, ModuleCommonForm

__all__ = (
    'CableBundleForm',
    'CableForm',
    'ConsolePortForm',
    'ConsolePortTemplateForm',
    'ConsoleServerPortForm',
    'ConsoleServerPortTemplateForm',
    'DeviceBayForm',
    'DeviceBayTemplateForm',
    'DeviceForm',
    'DeviceRoleForm',
    'DeviceTypeForm',
    'DeviceVCMembershipForm',
    'FrontPortForm',
    'FrontPortTemplateForm',
    'InterfaceForm',
    'InterfaceTemplateForm',
    'InventoryItemForm',
    'InventoryItemRoleForm',
    'InventoryItemTemplateForm',
    'LocationForm',
    'MACAddressForm',
    'ManufacturerForm',
    'ModuleBayForm',
    'ModuleBayTemplateForm',
    'ModuleBayTypeForm',
    'ModuleForm',
    'ModuleTypeForm',
    'ModuleTypeProfileForm',
    'PlatformForm',
    'PopulateDeviceBayForm',
    'PowerFeedForm',
    'PowerOutletForm',
    'PowerOutletTemplateForm',
    'PowerPanelForm',
    'PowerPortForm',
    'PowerPortTemplateForm',
    'RackForm',
    'RackGroupForm',
    'RackReservationForm',
    'RackRoleForm',
    'RackTypeForm',
    'RearPortForm',
    'RearPortTemplateForm',
    'RegionForm',
    'SiteForm',
    'SiteGroupForm',
    'VCMemberSelectForm',
    'VirtualChassisForm',
    'VirtualDeviceContextForm'
)


class RegionForm(NestedGroupModelForm):
    parent = DynamicModelChoiceField(
        label=_('Parent'),
        queryset=Region.objects.all(),
        required=False
    )

    fieldsets = (
        FieldSet('parent', 'name', 'slug', 'description', 'tags'),
    )

    class Meta:
        model = Region
        fields = (
            'parent', 'name', 'slug', 'description', 'owner', 'tags', 'comments',
        )


class SiteGroupForm(NestedGroupModelForm):
    parent = DynamicModelChoiceField(
        label=_('Parent'),
        queryset=SiteGroup.objects.all(),
        required=False
    )

    fieldsets = (
        FieldSet('parent', 'name', 'slug', 'description', 'tags'),
    )

    class Meta:
        model = SiteGroup
        fields = (
            'parent', 'name', 'slug', 'description', 'owner', 'comments', 'tags',
        )


class SiteForm(TenancyForm, PrimaryModelForm):
    status = ChoiceField(
        label=_('Status'),
        choices=SiteStatusChoices,
        initial=SiteStatusChoices.STATUS_ACTIVE,
    )
    region = DynamicModelChoiceField(
        label=_('Region'),
        queryset=Region.objects.all(),
        required=False,
        quick_add=True
    )
    group = DynamicModelChoiceField(
        label=_('Group'),
        queryset=SiteGroup.objects.all(),
        required=False,
        quick_add=True
    )
    asns = DynamicModelMultipleChoiceField(
        queryset=ASN.objects.all(),
        label=_('ASNs'),
        required=False
    )
    add_asns = DynamicModelMultipleChoiceField(
        queryset=ASN.objects.all(),
        label=_('Add ASNs'),
        required=False
    )
    remove_asns = DynamicModelMultipleChoiceField(
        queryset=ASN.objects.all(),
        label=_('Remove ASNs'),
        required=False
    )
    slug = SlugField()
    time_zone = TimeZoneFormField(
        label=_('Time zone'),
        choices=add_blank_choice(TimeZoneFormField().choices),
        required=False
    )

    fieldsets = (
        FieldSet(
            'name', 'slug', 'status', 'region', 'group', 'facility', M2MAddRemoveFields('asns'), 'time_zone',
            'description', 'tags',
            name=_('Site')
        ),
        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
        FieldSet('physical_address', 'shipping_address', 'latitude', 'longitude', name=_('Contact Info')),
    )

    class Meta:
        model = Site
        fields = (
            'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'time_zone',
            'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'owner', 'comments', 'tags',
        )
        widgets = {
            'physical_address': forms.Textarea(
                attrs={
                    'rows': 3,
                }
            ),
            'shipping_address': forms.Textarea(
                attrs={
                    'rows': 3,
                }
            ),
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.instance.pk and (count := self.instance.asns.count()) >= M2MAddRemoveFields.THRESHOLD:
            # Add/remove mode for large M2M sets
            self.fields.pop('asns')
            self.fields['add_asns'].widget.add_query_param('site_id__n', self.instance.pk)
            self.fields['remove_asns'].widget.add_query_param('site_id', self.instance.pk)
            self.fields['remove_asns'].help_text = _("{count} ASNs currently assigned").format(count=count)
        else:
            # Simple mode for new objects or small M2M sets
            self.fields.pop('add_asns')
            self.fields.pop('remove_asns')
            if self.instance.pk:
                self.initial['asns'] = list(self.instance.asns.values_list('pk', flat=True))


class LocationForm(TenancyForm, NestedGroupModelForm):
    status = ChoiceField(
        label=_('Status'),
        choices=LocationStatusChoices,
        initial=LocationStatusChoices.STATUS_ACTIVE,
    )
    site = DynamicModelChoiceField(
        label=_('Site'),
        queryset=Site.objects.all(),
        selector=True
    )
    parent = DynamicModelChoiceField(
        label=_('Parent'),
        queryset=Location.objects.all(),
        required=False,
        query_params={
            'site_id': '$site'
        }
    )

    fieldsets = (
        FieldSet('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags', name=_('Location')),
        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
    )

    class Meta:
        model = Location
        fields = (
            'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'facility', 'owner',
            'comments', 'tags',
        )


class RackGroupForm(OrganizationalModelForm):
    fieldsets = (
        FieldSet('name', 'slug', 'description', 'tags', name=_('Rack Group')),
    )

    class Meta:
        model = RackGroup
        fields = [
            'name', 'slug', 'description', 'owner', 'comments', 'tags',
        ]


class RackRoleForm(OrganizationalModelForm):
    fieldsets = (
        FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Rack Role')),
    )

    class Meta:
        model = RackRole
        fields = [
            'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
        ]


class RackTypeForm(PrimaryModelForm):
    form_factor = ChoiceField(
        label=_('Form factor'),
        choices=RackFormFactorChoices,
    )
    outer_unit = TypedChoiceField(
        label=_('Outer unit'),
        choices=add_blank_choice(RackDimensionUnitChoices),
        required=False,
    )
    weight_unit = TypedChoiceField(
        label=_('Weight unit'),
        choices=add_blank_choice(WeightUnitChoices),
        required=False,
    )
    manufacturer = DynamicModelChoiceField(
        label=_('Manufacturer'),
        queryset=Manufacturer.objects.all(),
        quick_add=True
    )
    slug = SlugField(
        label=_('Slug'),
        slug_source='model'
    )

    fieldsets = (
        FieldSet('manufacturer', 'model', 'slug', 'description', 'form_factor', 'tags', name=_('Rack Type')),
        FieldSet(
            'width', 'u_height',
            InlineFields('outer_width', 'outer_height', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
            InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
            'mounting_depth', name=_('Dimensions')
        ),
        FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
    )

    class Meta:
        model = RackType
        fields = [
            'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
            'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
            'weight_unit', 'description', 'owner', 'comments', 'tags',
        ]


class RackForm(TenancyForm, PrimaryModelForm):
    status = ChoiceField(
        label=_('Status'),
        choices=RackStatusChoices,
        initial=RackStatusChoices.STATUS_ACTIVE,
    )
    form_factor = TypedChoiceField(
        label=_('Form factor'),
        choices=add_blank_choice(RackFormFactorChoices),
        required=False,
    )
    outer_unit = TypedChoiceField(
        label=_('Outer unit'),
        choices=add_blank_choice(RackDimensionUnitChoices),
        required=False,
    )
    airflow = TypedChoiceField(
        label=_('Airflow'),
        choices=add_blank_choice(RackAirflowChoices),
        required=False,
    )
    weight_unit = TypedChoiceField(
        label=_('Weight unit'),
        choices=add_blank_choice(WeightUnitChoices),
        required=False,
    )
    site = DynamicModelChoiceField(
        label=_('Site'),
        queryset=Site.objects.all(),
        selector=True
    )
    location = DynamicModelChoiceField(
        label=_('Location'),
        queryset=Location.objects.all(),
        required=False,
        query_params={
            'site_id': '$site'
        }
    )
    group = DynamicModelChoiceField(
        label=_('Rack Group'),
        queryset=RackGroup.objects.all(),
        required=False
    )
    role = DynamicModelChoiceField(
        label=_('Role'),
        queryset=RackRole.objects.all(),
        required=False
    )
    rack_type = DynamicModelChoiceField(
        label=_('Rack Type'),
        queryset=RackType.objects.all(),
        required=False,
        selector=True,
        help_text=_("Select a pre-defined rack type, or set physical characteristics below."),
    )

    fieldsets = (
        FieldSet(
            'site', 'location', 'group', 'name', 'status', 'role', 'rack_type', 'description', 'airflow', 'tags',
            name=_('Rack')
        ),
        FieldSet('facility_id', 'serial', 'asset_tag', name=_('Inventory Control')),
        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
    )

    class Meta:
        model = Rack
        fields = [
            'site', 'location', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
            'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
            'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight',
            'weight_unit', 'description', 'owner', 'comments', 'tags',
        ]

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

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

        # Omit RackType-defined fields if rack_type is set
        if get_field_value(self, 'rack_type'):
            for field_name in Rack.RACKTYPE_FIELDS:
                del self.fields[field_name]
        else:
            self.fieldsets = (
                *self.fieldsets,
                FieldSet(
                    'form_factor', 'width', 'starting_unit', 'u_height',
                    InlineFields('outer_width', 'outer_height', 'outer_depth', 'outer_unit',
                                 label=_('Outer Dimensions')),
                    InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
                    'mounting_depth', 'desc_units', name=_('Dimensions')
                ),
            )


class RackReservationForm(TenancyForm, PrimaryModelForm):
    status = ChoiceField(
        label=_('Status'),
        choices=RackReservationStatusChoices,
        initial=RackReservationStatusChoices.STATUS_ACTIVE,
    )
    rack = DynamicModelChoiceField(
        label=_('Rack'),
        queryset=Rack.objects.all(),
        selector=True
    )
    units = NumericArrayField(
        label=_('Units'),
        base_field=forms.IntegerField(),
        help_text=_("Comma-separated list of numeric unit IDs. A range may be specified using a hyphen.")
    )
    user = forms.ModelChoiceField(
        label=_('User'),
        queryset=User.objects.order_by('username')
    )

    fieldsets = (
        FieldSet('rack', 'units', 'status', 'user', 'description', 'tags', name=_('Reservation')),
        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
    )

    class Meta:
        model = RackReservation
        fields = [
            'rack', 'units', 'status', 'user', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags',
        ]


class ManufacturerForm(OrganizationalModelForm):
    fieldsets = (
        FieldSet('name', 'slug', 'description', 'tags', name=_('Manufacturer')),
    )

    class Meta:
        model = Manufacturer
        fields = [
            'name', 'slug', 'description', 'owner', 'comments', 'tags',
        ]


class DeviceTypeForm(PrimaryModelForm):
    subdevice_role = TypedChoiceField(
        label=_('Parent/child status'),
        choices=add_blank_choice(SubdeviceRoleChoices),
        required=False,
        help_text=_(
            'Parent devices house child devices in device bays. Leave blank if this device type is neither a '
            'parent nor a child.'
        ),
    )
    airflow = TypedChoiceField(
        label=_('Airflow'),
        choices=add_blank_choice(DeviceAirflowChoices),
        required=False,
    )
    weight_unit = TypedChoiceField(
        label=_('Weight unit'),
        choices=add_blank_choice(WeightUnitChoices),
        required=False,
    )
    manufacturer = DynamicModelChoiceField(
        label=_('Manufacturer'),
        queryset=Manufacturer.objects.all(),
        quick_add=True
    )
    default_platform = DynamicModelChoiceField(
        label=_('Default platform'),
        queryset=Platform.objects.all(),
        required=False,
        selector=True,
        query_params={
            'manufacturer_id': ['$manufacturer', 'null'],
        }
    )
    slug = SlugField(
        label=_('Slug'),
        slug_source='model'
    )

    fieldsets = (
        FieldSet('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags', name=_('Device Type')),
        FieldSet(
            'u_height', 'exclude_from_utilization', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow',
            'weight', 'weight_unit', name=_('Chassis')
        ),
        FieldSet('end_of_life', name=_('Lifecycle')),
        FieldSet('front_image', 'rear_image', name=_('Images')),
    )

    class Meta:
        model = DeviceType
        fields = [
            'manufacturer', 'model', 'slug', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization',
            'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'end_of_life', 'front_image',
            'rear_image', 'description', 'owner', 'comments', 'tags',
        ]
        widgets = {
            'front_image': ClearableFileInput(attrs={
                'accept': DEVICETYPE_IMAGE_FORMATS
            }),
            'rear_image': ClearableFileInput(attrs={
                'accept': DEVICETYPE_IMAGE_FORMATS
            }),
            'end_of_life': DatePicker(),
        }


class ModuleBayTypeForm(PrimaryModelForm):
    manufacturer = DynamicModelChoiceField(
        label=_('Manufacturer'),
        queryset=Manufacturer.objects.all(),
        required=False,
    )
    slug = SlugField(
        slug_source='name',
    )

    fieldsets = (
        FieldSet('name', 'slug', 'manufacturer', 'color', 'description', 'tags', name=_('Module Bay Type')),
    )

    class Meta:
        model = ModuleBayType
        fields = [
            'name', 'slug', 'manufacturer', 'color', 'description', 'owner', 'comments', 'tags',
        ]


class ModuleTypeProfileForm(PrimaryModelForm):
    schema = JSONField(
        label=_('Schema'),
        required=False,
        help_text=_("Enter a valid JSON schema to define supported attributes.")
    )

    fieldsets = (
        FieldSet('name', 'description', 'schema', 'tags', name=_('Profile')),
    )

    class Meta:
        model = ModuleTypeProfile
        fields = [
            'name', 'description', 'schema', 'owner', 'comments', 'tags',
        ]


class ModuleTypeForm(PrimaryModelForm):
    airflow = TypedChoiceField(
        label=_('Airflow'),
        choices=add_blank_choice(ModuleAirflowChoices),
        required=False,
    )
    weight_unit = TypedChoiceField(
        label=_('Weight unit'),
        choices=add_blank_choice(WeightUnitChoices),
        required=False,
    )
    profile = forms.ModelChoiceField(
        queryset=ModuleTypeProfile.objects.all(),
        label=_('Profile'),
        required=False,
        widget=HTMXSelect(hx_target_id='profile-attributes')
    )
    manufacturer = DynamicModelChoiceField(
        label=_('Manufacturer'),
        queryset=Manufacturer.objects.all()
    )
    module_bay_types = DynamicModelMultipleChoiceField(
        label=_('Module bay types'),
        queryset=ModuleBayType.objects.all(),
        required=False,
    )

    @property
    def fieldsets(self):
        return [
            FieldSet('manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')),
            FieldSet('airflow', 'weight', 'weight_unit', name=_('Hardware')),
            FieldSet('module_bay_types', name=_('Bay Type Compatibility')),
            FieldSet('end_of_life', name=_('Lifecycle')),
            FieldSet('profile', *self.attr_fields, name=_('Profile & Attributes'), html_id='profile-attributes')
        ]

    class Meta:
        model = ModuleType
        fields = [
            'profile', 'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit',
            'module_bay_types', 'end_of_life', 'owner', 'comments', 'tags',
        ]
        widgets = {
            'end_of_life': DatePicker(),
        }

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

        # Track profile-specific attribute fields
        self.attr_fields = []

        # Retrieve assigned ModuleTypeProfile, if any
        if not (profile_id := get_field_value(self, 'profile')):
            return
        if not (profile := ModuleTypeProfile.objects.filter(pk=profile_id).first()):
            return

        # Extend form with fields for profile attributes
        for attr, form_field in self._get_attr_form_fields(profile).items():
            field_name = f'attr_{attr}'
            self.attr_fields.append(field_name)
            self.fields[field_name] = form_field
            if self.instance.attribute_data:
                self.fields[field_name].initial = self.instance.attribute_data.get(attr)

    @staticmethod
    def _get_attr_form_fields(profile):
        """
        Return a dictionary mapping of attribute names to form fields, suitable for extending
        the form per the selected ModuleTypeProfile.
        """
        if not profile.schema:
            return {}

        properties = profile.schema.get('properties', {})
        required_fields = profile.schema.get('required', [])

        attr_fields = {}
        for name, options in properties.items():
            prop = JSONSchemaProperty(**options)
            attr_fields[name] = prop.to_form_field(name, required=name in required_fields)

        return dict(sorted(attr_fields.items()))

    def _post_clean(self):

        # Compile attribute data from the individual form fields
        if self.cleaned_data.get('profile'):
            self.instance.attribute_data = {
                name[5:]: self.cleaned_data[name]  # Remove the attr_ prefix
                for name in self.attr_fields
                if self.cleaned_data.get(name) not in EMPTY_VALUES
            }

        return super()._post_clean()


class DeviceRoleForm(NestedGroupModelForm):
    config_template = DynamicModelChoiceField(
        label=_('Config template'),
        queryset=ConfigTemplate.objects.all(),
        required=False
    )
    parent = DynamicModelChoiceField(
        label=_('Parent'),
        queryset=DeviceRole.objects.all(),
        required=False,
    )

    fieldsets = (
        FieldSet(
            'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description',
            'tags', name=_('Device Role')
        ),
    )

    class Meta:
        model = DeviceRole
        fields = [
            'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description', 'owner', 'comments', 'tags',
        ]


class PlatformForm(NestedGroupModelForm):
    parent = DynamicModelChoiceField(
        label=_('Parent'),
        queryset=Platform.objects.all(),
        required=False,
    )
    manufacturer = DynamicModelChoiceField(
        label=_('Manufacturer'),
        queryset=Manufacturer.objects.all(),
        required=False,
        quick_add=True
    )
    config_template = DynamicModelChoiceField(
        label=_('Config template'),
        queryset=ConfigTemplate.objects.all(),
        required=False
    )
    slug = SlugField(
        label=_('Slug'),
        max_length=64
    )

    fieldsets = (
        FieldSet(
            'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'tags', name=_('Platform'),
        ),
    )

    class Meta:
        model = Platform
        fields = [
            'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'owner', 'comments', 'tags',
        ]


class DeviceForm(TenancyForm, PrimaryModelForm):
    status = ChoiceField(
        label=_('Status'),
        choices=DeviceStatusChoices,
        initial=DeviceStatusChoices.STATUS_ACTIVE,
    )
    airflow = TypedChoiceField(
        label=_('Airflow'),
        choices=add_blank_choice(DeviceAirflowChoices),
        required=False,
    )
    site = DynamicModelChoiceField(
        label=_('Site'),
        queryset=Site.objects.all(),
        selector=True
    )
    location = DynamicModelChoiceField(
        label=_('Location'),
        queryset=Location.objects.all(),
        required=False,
        query_params={
            'site_id': '$site'
        },
        initial_params={
            'racks': '$rack'
        }
    )
    rack = DynamicModelChoiceField(
        label=_('Rack'),
        queryset=Rack.objects.all(),
        required=False,
        query_params={
            'site_id': '$site',
            'location_id': '$location',
        }
    )
    position = forms.DecimalField(
        label=_('Position'),
        required=False,
        help_text=_("The lowest-numbered unit occupied by the device"),
        localize=True,
        widget=APISelect(
            api_url='/api/dcim/racks/{{rack}}/elevation/',
            attrs={
                'ts-disabled-field': 'device',
                'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
            },
        )
    )
    face = TypedChoiceField(
        label=_('Face'),
        choices=add_blank_choice(DeviceFaceChoices),
        required=False,
        widget=ClearableSelect(
            requires_fields=['rack']
        )
    )
    device_type = DynamicModelChoiceField(
        label=_('Device type'),
        queryset=DeviceType.objects.all(),
        context={
            'parent': 'manufacturer',
        },
        selector=True
    )
    role = DynamicModelChoiceField(
        label=_('Device role'),
        queryset=DeviceRole.objects.all(),
        quick_add=True
    )
    platform = DynamicModelChoiceField(
        label=_('Platform'),
        queryset=Platform.objects.all(),
        required=False,
        selector=True,
        query_params={
            'available_for_device_type': '$device_type',
        }
    )
    cluster = DynamicModelChoiceField(
        label=_('Cluster'),
        queryset=Cluster.objects.all(),
        required=False,
        selector=True,
        query_params={
            'site_id': ['$site', 'null']
        },
    )
    local_context_data = JSONField(
        required=False,
        label='',
        widget=forms.Textarea(attrs={'aria-label': _('Local config context data')})
    )
    virtual_chassis = DynamicModelChoiceField(
        label=_('Virtual chassis'),
        queryset=VirtualChassis.objects.all(),
        required=False,
        context={
            'parent': 'master',
        },
        selector=True
    )
    vc_position = forms.IntegerField(
        required=False,
        label=_('Position'),
        help_text=_("The position in the virtual chassis this device is identified by")
    )
    vc_priority = forms.IntegerField(
        required=False,
        label=_('Priority'),
        help_text=_("The priority of the device in the virtual chassis")
    )
    config_template = DynamicModelChoiceField(
        label=_('Config template'),
        queryset=ConfigTemplate.objects.all(),
        required=False
    )

    class Meta:
        model = Device
        fields = [
            'name', 'role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face',
            'latitude', 'longitude', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster',
            'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template',
            'owner', 'comments', 'tags', 'local_context_data',
        ]

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

        if self.instance.pk:

            # Compile list of choices for primary IPv4 and IPv6 addresses
            oob_ip_choices = [(None, '---------')]
            for family in [4, 6]:
                ip_choices = [(None, '---------')]

                # Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member
                interface_ids = self.instance.vc_interfaces(if_master=False).values_list('pk', flat=True)

                # Collect interface IPs
                interface_ips = IPAddress.objects.filter(
                    address__family=family,
                    assigned_object_type=ContentType.objects.get_for_model(Interface),
                    assigned_object_id__in=interface_ids
                ).prefetch_related('assigned_object')
                if interface_ips:
                    ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips]
                    ip_choices.append(('Interface IPs', ip_list))
                    oob_ip_choices.extend(ip_list)
                # Collect NAT IPs
                nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
                    address__family=family,
                    nat_inside__assigned_object_type=ContentType.objects.get_for_model(Interface),
                    nat_inside__assigned_object_id__in=interface_ids
                ).prefetch_related('assigned_object')
                if nat_ips:
                    ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips]
                    ip_choices.append(('NAT IPs', ip_list))
                self.fields['primary_ip{}'.format(family)].choices = ip_choices
            self.fields['oob_ip'].choices = oob_ip_choices

            # If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
            # can be flipped from one face to another.
            self.fields['position'].widget.add_query_param('exclude', self.instance.pk)

            # Disable rack assignment if this is a child device installed in a parent device
            if self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
                self.fields['site'].disabled = True
                self.fields['rack'].disabled = True
                self.initial['site'] = self.instance.parent_bay.device.site_id
                self.initial['rack'] = self.instance.parent_bay.device.rack_id

        else:

            # An object that doesn't exist yet can't have any IPs assigned to it
            self.fields['primary_ip4'].choices = []
            self.fields['primary_ip4'].widget.attrs['readonly'] = True
            self.fields['primary_ip6'].choices = []
            self.fields['primary_ip6'].widget.attrs['readonly'] = True
            self.fields['oob_ip'].choices = []
            self.fields['oob_ip'].widget.attrs['readonly'] = True

        # Rack position
        position = self.data.get('position') or self.initial.get('position')
        if position:
            self.fields['position'].widget.choices = [(position, f'U{position}')]


class ModuleForm(ModuleCommonForm, PrimaryModelForm):
    status = ChoiceField(
        label=_('Status'),
        choices=ModuleStatusChoices,
        initial=ModuleStatusChoices.STATUS_ACTIVE,
    )
    device = DynamicModelChoiceField(
        label=_('Device'),
        queryset=Device.objects.all(),
        initial_params={
            'modulebays': '$module_bay'
        }
    )
    module_bay = DynamicModelChoiceField(
        label=_('Module bay'),
        queryset=ModuleBay.objects.all(),
        query_params={
            'device_id': '$device',
        },
        context={
            'disabled': '_occupied',
        },
    )
    module_type = DynamicModelChoiceField(
        label=_('Module type'),
        queryset=ModuleType.objects.all(),
        context={
            'parent': 'manufacturer',
        },
        selector=True
    )
    replicate_components = forms.BooleanField(
        label=_('Replicate components'),
        required=False,
        initial=True,
        help_text=_("Automatically populate components associated with this module type")
    )
    adopt_components = forms.BooleanField(
        label=_('Adopt components'),
        required=False,
        initial=False,
        help_text=_("Adopt already existing components")
    )

    fieldsets = (
        FieldSet('device', 'module_bay', 'module_type', 'status', 'description', 'tags', name=_('Module')),
        FieldSet('serial', 'asset_tag', 'replicate_components', 'adopt_components', name=_('Hardware')),
    )

    class Meta:
        model = Module
        fields = [
            'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', 'tags', 'replicate_components',
            'adopt_components', 'description', 'owner', 'comments',
        ]

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

        if self.instance.pk:
            self.fields['replicate_components'].initial = False
            self.fields['replicate_components'].disabled = True
            self.fields['adopt_components'].initial = False
            self.fields['adopt_components'].disabled = True


def get_termination_type_choices():
    return add_blank_choice([
        Choice(f'{ct.app_label}.{ct.model}', ct.model_class()._meta.verbose_name.title())
        for ct in ContentType.objects.filter(CABLE_TERMINATION_MODELS)
    ])


class CableBundleForm(PrimaryModelForm):

    fieldsets = (
        FieldSet('name', 'description', 'tags', name=_('Cable Bundle')),
    )

    class Meta:
        model = CableBundle
        fields = ['name', 'description', 'owner', 'comments', 'tags']


class CableForm(TenancyForm, PrimaryModelForm):
    type = TypedChoiceField(
        label=_('Type'),
        choices=add_blank_choice(CableTypeChoices),
        required=False,
    )
    status = ChoiceField(
        label=_('Status'),
        choices=LinkStatusChoices,
        initial=LinkStatusChoices.STATUS_CONNECTED,
    )
    profile = ChoiceField(
        label=_('Profile'),
        choices=add_blank_choice(CableProfileChoices),
        required=False,
    )
    length_unit = TypedChoiceField(
        label=_('Length unit'),
        choices=add_blank_choice(CableLengthUnitChoices),
        required=False,
    )
    a_terminations_type = ChoiceField(
        choices=get_termination_type_choices,
        required=False,
        widget=HTMXSelect(hx_target_id='cable-side-a'),
        label=_('Type')
    )
    b_terminations_type = ChoiceField(
        choices=get_termination_type_choices,
        required=False,
        widget=HTMXSelect(hx_target_id='cable-side-b'),
        label=_('Type')
    )
    bundle = DynamicModelChoiceField(
        queryset=CableBundle.objects.all(),
        required=False,
        label=_('Bundle'),
    )

    class Meta:
        model = Cable
        fields = [
            'a_terminations_type', 'b_terminations_type', 'type', 'status', 'profile', 'tenant_group', 'tenant',
            'bundle', 'label', 'color', 'length', 'length_unit', 'description', 'owner', 'comments', 'tags',
        ]


class PowerPanelForm(PrimaryModelForm):
    site = DynamicModelChoiceField(
        label=_('Site'),
        queryset=Site.objects.all(),
        selector=True
    )
    location = DynamicModelChoiceField(
        label=_('Location'),
        queryset=Location.objects.all(),
        required=False,
        query_params={
            'site_id': '$site'
        }
    )

    fieldsets = (
        FieldSet('site', 'location', 'name', 'description', 'tags', name=_('Power Panel')),
    )

    class Meta:
        model = PowerPanel
        fields = [
            'site', 'location', 'name', 'description', 'owner', 'comments', 'tags',
        ]


class PowerFeedForm(TenancyForm, PrimaryModelForm):
    status = ChoiceField(
        label=_('Status'),
        choices=PowerFeedStatusChoices,
        initial=PowerFeedStatusChoices.STATUS_ACTIVE,
    )
    type = ChoiceField(
        label=_('Type'),
        choices=PowerFeedTypeChoices,
        initial=PowerFeedTypeChoices.TYPE_PRIMARY,
    )
    supply = ChoiceField(
        label=_('Supply'),
        choices=PowerFeedSupplyChoices,
        initial=PowerFeedSupplyChoices.SUPPLY_AC,
    )
    phase = ChoiceField(
        label=_('Phase'),
        choices=PowerFeedPhaseChoices,
        initial=PowerFeedPhaseChoices.PHASE_SINGLE,
    )
    power_panel = DynamicModelChoiceField(
        label=_('Power panel'),
        queryset=PowerPanel.objects.all(),
        selector=True,
        quick_add=True
    )
    rack = DynamicModelChoiceField(
        label=_('Rack'),
        queryset=Rack.objects.all(),
        required=False,
        selector=True
    )

    fieldsets = (
        FieldSet(
            'power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags',
            name=_('Power Feed')
        ),
        FieldSet('supply', 'voltage', 'amperage', 'phase', 'max_utilization', name=_('Characteristics')),
        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
    )

    class Meta:
        model = PowerFeed
        fields = [
            'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
            'max_utilization', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags'
        ]


#
# Virtual chassis
#

class VirtualChassisForm(PrimaryModelForm):
    master = forms.ModelChoiceField(
        label=_('Master'),
        queryset=Device.objects.all(),
        required=False,
    )

    class Meta:
        model = VirtualChassis
        fields = [
            'name', 'domain', 'master', 'description', 'owner', 'comments', 'tags',
        ]
        widgets = {
            'master': SelectWithPK(),
        }

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

        self.fields['master'].queryset = Device.objects.filter(virtual_chassis=self.instance)


class DeviceVCMembershipForm(forms.ModelForm):
    class Meta:
        model = Device
        fields = [
            'vc_position', 'vc_priority',
        ]
        labels = {
            'vc_position': 'Position',
            'vc_priority': 'Priority',
        }

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

        # Require VC position (only required when the Device is a VirtualChassis member)
        self.fields['vc_position'].required = True

        # Add bootstrap classes to form elements.
        self.fields['vc_position'].widget.attrs = {'class': 'form-control'}
        self.fields['vc_priority'].widget.attrs = {'class': 'form-control'}

        # Validation of vc_position is optional. This is only required when adding a new member to an existing
        # VirtualChassis. Otherwise, vc_position validation is handled by BaseVCMemberFormSet.
        self.validate_vc_position = validate_vc_position

    def clean_vc_position(self):
        vc_position = self.cleaned_data['vc_position']

        if self.validate_vc_position:
            conflicting_members = Device.objects.filter(
                virtual_chassis=self.instance.virtual_chassis,
                vc_position=vc_position
            )
            if conflicting_members.exists():
                raise forms.ValidationError(
                    'A virtual chassis member already exists in position {}.'.format(vc_position)
                )

        return vc_position


class VCMemberSelectForm(forms.Form):
    device = DynamicModelChoiceField(
        label=_('Device'),
        queryset=Device.objects.all(),
        query_params={
            'virtual_chassis_id': 'null',
        },
        selector=True
    )

    def clean_device(self):
        device = self.cleaned_data['device']
        if device.virtual_chassis is not None:
            raise forms.ValidationError(
                f"Device {device} is already assigned to a virtual chassis."
            )
        return device


#
# Device component templates
#

class ComponentTemplateForm(ChangelogMessageMixin, forms.ModelForm):
    device_type = DynamicModelChoiceField(
        label=_('Device type'),
        queryset=DeviceType.objects.all(),
        context={
            'parent': 'manufacturer',
        }
    )

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

        # Disable reassignment of DeviceType when editing an existing instance
        if self.instance.pk:
            self.fields['device_type'].disabled = True


class ModularComponentTemplateForm(ComponentTemplateForm):
    device_type = DynamicModelChoiceField(
        label=_('Device type'),
        queryset=DeviceType.objects.all(),
        required=False,
        context={
            'parent': 'manufacturer',
        }
    )
    module_type = DynamicModelChoiceField(
        label=_('Module type'),
        queryset=ModuleType.objects.all(),
        required=False,
        context={
            'parent': 'manufacturer',
        }
    )

    fieldsets = (
        FieldSet(
            TabbedGroups(
                FieldSet('device_type', name=_('Device Type')),
                FieldSet('module_type', name=_('Module Type')),
            ),
            'name', 'label', 'type', 'description'
        ),
    )

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

        # Disable reassignment of ModuleType when editing an existing instance
        if self.instance.pk:
            self.fields['module_type'].disabled = True

        # Components attached to a module need to present this standardized substitution help text.
        self.fields['name'].help_text = _(
            "Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range are not "
            "supported (example: <code>[ge,xe]-0/0/[0-9]</code>). The token <code>{module}</code>, if present, will be "
            "automatically replaced with the position value when creating a new module. "
            "The token <code>{vc_position}</code> will be replaced with the device's Virtual Chassis position "
            "(use <code>{vc_position:1}</code> to specify a fallback (default is 0))"
        )


class ConsolePortTemplateForm(ModularComponentTemplateForm):
    type = TypedChoiceField(
        label=_('Type'),
        choices=add_blank_choice(ConsolePortTypeChoices),
        required=False,
    )

    class Meta:
        model = ConsolePortTemplate
        fields = [
            'device_type', 'module_type', 'name', 'label', 'type', 'description',
        ]


class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
    type = TypedChoiceField(
        label=_('Type'),
        choices=add_blank_choice(ConsolePortTypeChoices),
        required=False,
    )

    class Meta:
        model = ConsoleServerPortTemplate
        fields = [
            'device_type', 'module_type', 'name', 'label', 'type', 'description',
        ]


class PowerPortTemplateForm(ModularComponentTemplateForm):
    type = TypedChoiceField(
        label=_('Type'),
        choices=add_blank_choice(PowerPortTypeChoices),
        required=False,
    )
    fieldsets = (
        FieldSet(
            TabbedGroups(
                FieldSet('device_type', name=_('Device Type')),
                FieldSet('module_type', name=_('Module Type')),
            ),
            'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
        ),
    )

    class Meta:
        model = PowerPortTemplate
        fields = [
            'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
        ]


class PowerOutletTemplateForm(ModularComponentTemplateForm):
    type = TypedChoiceField(
        label=_('Type'),
        choices=add_blank_choice(PowerOutletTypeChoices),
        required=False,
    )
    feed_leg = TypedChoiceField(
        label=_('Feed leg'),
        choices=add_blank_choice(PowerOutletFeedLegChoices),
        required=False,
        help_text=_('Phase (for three-phase feeds)'),
    )
    power_port = DynamicModelChoiceField(
        label=_('Power port'),
        queryset=PowerPortTemplate.objects.all(),
        required=False,
        query_params={
            'device_type_id': '$device_type',
        }
    )

    fieldsets = (
        FieldSet(
            TabbedGroups(
                FieldSet('device_type', name=_('Device Type')),
                FieldSet('module_type', name=_('Module Type')),
            ),
            'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'description',
        ),
    )

    class Meta:
        model = PowerOutletTemplate
        fields = [
            'device_type', 'module_type', 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'description',
        ]


class InterfaceTemplateForm(ModularComponentTemplateForm):
    type = ChoiceField(
        label=_('Type'),
        choices=InterfaceTypeChoices,
    )
    poe_mode = TypedChoiceField(
        label=_('PoE mode'),
        choices=add_blank_choice(InterfacePoEModeChoices),
        required=False,
    )
    poe_type = TypedChoiceField(
        label=_('PoE type'),
        choices=add_blank_choice(InterfacePoETypeChoices),
        required=False,
    )
    rf_role = TypedChoiceField(
        label=_('Wireless role'),
        choices=add_blank_choice(WirelessRoleChoices),
        required=False,
    )
    parent = DynamicModelChoiceField(
        label=_('Parent'),
        queryset=InterfaceTemplate.objects.all(),
        required=False,
        query_params={
            'device_type_id': '$device_type',
            'module_type_id': '$module_type',
        }
    )
    bridge = DynamicModelChoiceField(
        label=_('Bridge'),
        queryset=InterfaceTemplate.objects.all(),
        required=False,
        query_params={
            'device_type_id': '$device_type',
            'module_type_id': '$module_type',
        }
    )

    fieldsets = (
        FieldSet(
            TabbedGroups(
                FieldSet('device_type', name=_('Device Type')),
                FieldSet('module_type', name=_('Module Type')),
            ),
            'name', 'label', 'type', 'channels', 'channel_id', 'enabled', 'mgmt_only', 'description', 'parent',
            'bridge',
        ),
        FieldSet('poe_mode', 'poe_type', name=_('PoE')),
        FieldSet('rf_role', name=_('Wireless')),
    )

    class Meta:
        model = InterfaceTemplate
        fields = [
            'device_type', 'module_type', 'name', 'label', 'type', 'channels', 'channel_id', 'mgmt_only', 'enabled',
            'description', 'poe_mode', 'poe_type', 'parent', 'bridge', 'rf_role',
        ]


class FrontPortTemplateForm(FrontPortFormMixin, ModularComponentTemplateForm):
    type = ChoiceField(
        label=_('Type'),
        choices=PortTypeChoices,
    )
    fieldsets = (
        FieldSet(
            TabbedGroups(
                FieldSet('device_type', name=_('Device Type')),
                FieldSet('module_type', name=_('Module Type')),
            ),
            'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'description',
        ),
    )

    port_mapping_model = PortTemplateMapping
    rear_port_model = RearPortTemplate

    class Meta:
        model = FrontPortTemplate
        fields = [
            'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
        ]

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

        # Populate rear port choices based on parent DeviceType or ModuleType
        if device_type_id := self.data.get('device_type') or self.initial.get('device_type'):
            parent_filter = Q(device_type=device_type_id)
        elif module_type_id := self.data.get('module_type') or self.initial.get('module_type'):
            parent_filter = Q(module_type=module_type_id)
        else:
            return
        self.fields['rear_ports'].choices = self._get_rear_port_choices(parent_filter, self.instance)

        # Set initial rear port mappings
        if self.instance.pk:
            self.initial['rear_ports'] = [
                f'{mapping.rear_port_id}:{mapping.rear_port_position}'
                for mapping in PortTemplateMapping.objects.filter(front_port_id=self.instance.pk)
            ]


class RearPortTemplateForm(ModularComponentTemplateForm):
    type = ChoiceField(
        label=_('Type'),
        choices=PortTypeChoices,
    )
    fieldsets = (
        FieldSet(
            TabbedGroups(
                FieldSet('device_type', name=_('Device Type')),
                FieldSet('module_type', name=_('Module Type')),
            ),
            'name', 'label', 'type', 'color', 'positions', 'description',
        ),
    )

    class Meta:
        model = RearPortTemplate
        fields = [
            'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
        ]


class ModuleBayTemplateForm(ModularComponentTemplateForm):
    module_bay_types = DynamicModelMultipleChoiceField(
        label=_('Module bay types'),
        queryset=ModuleBayType.objects.all(),
        required=False,
    )

    fieldsets = (
        FieldSet(
            TabbedGroups(
                FieldSet('device_type', name=_('Device Type')),
                FieldSet('module_type', name=_('Module Type')),
            ),
            'name', 'label', 'position', 'enabled', 'description', 'module_bay_types',
        ),
    )

    class Meta:
        model = ModuleBayTemplate
        fields = [
            'device_type', 'module_type', 'name', 'label', 'position', 'enabled', 'description', 'module_bay_types',
        ]


class DeviceBayTemplateForm(ComponentTemplateForm):
    fieldsets = (
        FieldSet('device_type', 'name', 'label', 'enabled', 'description'),
    )

    class Meta:
        model = DeviceBayTemplate
        fields = [
            'device_type', 'name', 'label', 'enabled', 'description',
        ]


class InventoryItemTemplateForm(ComponentTemplateForm):
    parent = DynamicModelChoiceField(
        label=_('Parent'),
        queryset=InventoryItemTemplate.objects.all(),
        required=False,
        query_params={
            'device_type_id': '$device_type'
        }
    )
    role = DynamicModelChoiceField(
        label=_('Role'),
        queryset=InventoryItemRole.objects.all(),
        required=False
    )
    manufacturer = DynamicModelChoiceField(
        label=_('Manufacturer'),
        queryset=Manufacturer.objects.all(),
        required=False
    )

    # Assigned component selectors
    consoleporttemplate = DynamicModelChoiceField(
        queryset=ConsolePortTemplate.objects.all(),
        required=False,
        query_params={
            'device_type_id': '$device_type'
        },
        label=_('Console port template')
    )
    consoleserverporttemplate = DynamicModelChoiceField(
        queryset=ConsoleServerPortTemplate.objects.all(),
        required=False,
        query_params={
            'device_type_id': '$device_type'
        },
        label=_('Console server port template')
    )
    frontporttemplate = DynamicModelChoiceField(
        queryset=FrontPortTemplate.objects.all(),
        required=False,
        query_params={
            'device_type_id': '$device_type'
        },
        label=_('Front port template')
    )
    interfacetemplate = DynamicModelChoiceField(
        queryset=InterfaceTemplate.objects.all(),
        required=False,
        query_params={
            'device_type_id': '$device_type'
        },
        label=_('Interface template')
    )
    poweroutlettemplate = DynamicModelChoiceField(
        queryset=PowerOutletTemplate.objects.all(),
        required=False,
        query_params={
            'device_type_id': '$device_type'
        },
        label=_('Power outlet template')
    )
    powerporttemplate = DynamicModelChoiceField(
        queryset=PowerPortTemplate.objects.all(),
        required=False,
        query_params={
            'device_type_id': '$device_type'
        },
        label=_('Power port template')
    )
    rearporttemplate = DynamicModelChoiceField(
        queryset=RearPortTemplate.objects.all(),
        required=False,
        query_params={
            'device_type_id': '$device_type'
        },
        label=_('Rear port template')
    )

    fieldsets = (
        FieldSet(
            'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
        ),
        FieldSet(
            TabbedGroups(
                FieldSet('interfacetemplate', name=_('Interface')),
                FieldSet('consoleporttemplate', name=_('Console Port')),
                FieldSet('consoleserverporttemplate', name=_('Console Server Port')),
                FieldSet('frontporttemplate', name=_('Front Port')),
                FieldSet('rearporttemplate', name=_('Rear Port')),
                FieldSet('powerporttemplate', name=_('Power Port')),
                FieldSet('poweroutlettemplate', name=_('Power Outlet')),
            ),
            name=_('Component Assignment')
        )
    )

    class Meta:
        model = InventoryItemTemplate
        fields = [
            'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
        ]

    def __init__(self, *args, **kwargs):
        instance = kwargs.get('instance')
        initial = kwargs.get('initial', {}).copy()
        component_type = initial.get('component_type')
        component_id = initial.get('component_id')

        if instance:
            # When editing set the initial value for component selection
            for component_model in ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS):
                if type(instance.component) is component_model.model_class():
                    initial[component_model.model] = instance.component
                    break
        elif component_type and component_id:
            # When adding the InventoryItem from a component page
            content_type = ContentType.objects.filter(
                MODULAR_COMPONENT_TEMPLATE_MODELS
            ).filter(pk=component_type).first()
            if content_type:
                if component := content_type.model_class().objects.filter(pk=component_id).first():
                    initial[content_type.model] = component

        kwargs['initial'] = initial

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

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

        # Handle object assignment
        selected_objects = [
            field for field in (
                'consoleporttemplate', 'consoleserverporttemplate', 'frontporttemplate', 'interfacetemplate',
                'poweroutlettemplate', 'powerporttemplate', 'rearporttemplate'
            ) if self.cleaned_data[field]
        ]
        if len(selected_objects) > 1:
            raise forms.ValidationError(_("An InventoryItem can only be assigned to a single component."))
        if selected_objects:
            self.instance.component = self.cleaned_data[selected_objects[0]]
        else:
            self.instance.component = None


#
# Device components
#

class DeviceComponentForm(OwnerMixin, NetBoxModelForm):
    device = DynamicModelChoiceField(
        label=_('Device'),
        queryset=Device.objects.all(),
        selector=True
    )

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

        # Disable reassignment of Device when editing an existing instance
        if self.instance.pk:
            self.fields['device'].disabled = True


class ModularDeviceComponentForm(DeviceComponentForm):
    module = DynamicModelChoiceField(
        label=_('Module'),
        queryset=Module.objects.all(),
        required=False,
        query_params={
            'device_id': '$device',
        }
    )


class ConsolePortForm(ModularDeviceComponentForm):
    type = TypedChoiceField(
        label=_('Type'),
        choices=add_blank_choice(ConsolePortTypeChoices),
        required=False,
        help_text=_('Physical port type'),
    )
    fieldsets = (
        FieldSet(
            'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
        ),
    )

    class Meta:
        model = ConsolePort
        fields = [
            'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'owner', 'tags',
        ]


class ConsoleServerPortForm(ModularDeviceComponentForm):
    type = TypedChoiceField(
        label=_('Type'),
        choices=add_blank_choice(ConsolePortTypeChoices),
        required=False,
        help_text=_('Physical port type'),
    )
    fieldsets = (
        FieldSet(
            'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
        ),
    )

    class Meta:
        model = ConsoleServerPort
        fields = [
            'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'owner', 'tags',
        ]


class PowerPortForm(ModularDeviceComponentForm):
    type = TypedChoiceField(
        label=_('Type'),
        choices=add_blank_choice(PowerPortTypeChoices),
        required=False,
        help_text=_('Physical port type'),
    )
    fieldsets = (
        FieldSet(
            'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
            'description', 'tags',
        ),
    )

    class Meta:
        model = PowerPort
        fields = [
            'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
            'description', 'owner', 'tags',
        ]


class PowerOutletForm(ModularDeviceComponentForm):
    type = TypedChoiceField(
        label=_('Type'),
        choices=add_blank_choice(PowerOutletTypeChoices),
        required=False,
        help_text=_('Physical port type'),
    )
    status = ChoiceField(
        label=_('Status'),
        choices=PowerOutletStatusChoices,
        initial=PowerOutletStatusChoices.STATUS_ENABLED,
    )
    feed_leg = TypedChoiceField(
        label=_('Feed leg'),
        choices=add_blank_choice(PowerOutletFeedLegChoices),
        required=False,
        help_text=_('Phase (for three-phase feeds)'),
    )
    power_port = DynamicModelChoiceField(
        label=_('Power port'),
        queryset=PowerPort.objects.all(),
        required=False,
        query_params={
            'device_id': '$device',
        }
    )

    fieldsets = (
        FieldSet(
            'device', 'module', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'mark_connected',
            'description', 'owner', 'tags',
        ),
    )

    class Meta:
        model = PowerOutlet
        fields = [
            'device', 'module', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'mark_connected',
            'description', 'tags',
        ]


class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
    type = ChoiceField(
        label=_('Type'),
        choices=InterfaceTypeChoices,
    )
    duplex = TypedChoiceField(
        label=_('Duplex'),
        choices=add_blank_choice(InterfaceDuplexChoices),
        required=False,
    )
    poe_mode = TypedChoiceField(
        label=_('PoE mode'),
        choices=add_blank_choice(InterfacePoEModeChoices),
        required=False,
    )
    poe_type = TypedChoiceField(
        label=_('PoE type'),
        choices=add_blank_choice(InterfacePoETypeChoices),
        required=False,
    )
    mode = TypedChoiceField(
        label=_('802.1Q Mode'),
        choices=add_blank_choice(InterfaceModeChoices),
        required=False,
        help_text=_('IEEE 802.1Q tagging strategy'),
    )
    rf_role = TypedChoiceField(
        label=_('Wireless role'),
        choices=add_blank_choice(WirelessRoleChoices),
        required=False,
    )
    rf_channel = TypedChoiceField(
        label=_('Wireless channel'),
        choices=add_blank_choice(WirelessChannelChoices),
        required=False,
    )
    vdcs = DynamicModelMultipleChoiceField(
        queryset=VirtualDeviceContext.objects.all(),
        required=False,
        label=_('Virtual device contexts'),
        initial_params={
            'interfaces': '$parent',
        },
        query_params={
            'device_id': '$device',
        }
    )
    parent = DynamicModelChoiceField(
        queryset=Interface.objects.all(),
        required=False,
        label=_('Parent interface'),
        query_params={
            'virtual_chassis_member_id': '$device',
        }
    )
    bridge = DynamicModelChoiceField(
        queryset=Interface.objects.all(),
        required=False,
        label=_('Bridged interface'),
        query_params={
            'virtual_chassis_member_id': '$device',
        }
    )
    lag = DynamicModelChoiceField(
        queryset=Interface.objects.all(),
        required=False,
        label=_('LAG interface'),
        query_params={
            'virtual_chassis_member_id': '$device',
            'type': 'lag',
        }
    )
    wireless_lan_group = DynamicModelChoiceField(
        queryset=WirelessLANGroup.objects.all(),
        required=False,
        label=_('Wireless LAN group')
    )
    wireless_lans = DynamicModelMultipleChoiceField(
        queryset=WirelessLAN.objects.all(),
        required=False,
        label=_('Wireless LANs'),
        query_params={
            'group_id': '$wireless_lan_group',
        }
    )
    vlan_group = DynamicModelChoiceField(
        queryset=VLANGroup.objects.all(),
        required=False,
        label=_('VLAN group'),
        help_text=_("Filter VLANs available for assignment by group.")
    )
    untagged_vlan = DynamicModelChoiceField(
        queryset=VLAN.objects.all(),
        required=False,
        label=_('Untagged VLAN'),
        query_params={
            'group_id': '$vlan_group',
            'available_on_device': '$device',
        }
    )
    tagged_vlans = DynamicModelMultipleChoiceField(
        queryset=VLAN.objects.all(),
        required=False,
        label=_('Tagged VLANs'),
        query_params={
            'group_id': '$vlan_group',
            'available_on_device': '$device',
        }
    )
    qinq_svlan = DynamicModelChoiceField(
        queryset=VLAN.objects.all(),
        required=False,
        label=_('Q-in-Q Service VLAN'),
        query_params={
            'group_id': '$vlan_group',
            'available_on_device': '$device',
            'qinq_role': VLANQinQRoleChoices.ROLE_SERVICE,
        }
    )
    vrf = DynamicModelChoiceField(
        queryset=VRF.objects.all(),
        required=False,
        label=_('VRF')
    )
    wwn = forms.CharField(
        empty_value=None,
        required=False,
        label=_('WWN')
    )
    vlan_translation_policy = DynamicModelChoiceField(
        queryset=VLANTranslationPolicy.objects.all(),
        required=False,
        label=_('VLAN Translation Policy')
    )

    fieldsets = (
        FieldSet(
            'device', 'module', 'name', 'label', 'type', 'channels', 'speed', 'duplex', 'description', 'tags',
            name=_('Interface')
        ),
        FieldSet('vrf', 'mac_address', 'wwn', name=_('Addressing')),
        FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
        FieldSet('parent', 'channel_id', 'bridge', 'lag', name=_('Related Interfaces')),
        FieldSet('poe_mode', 'poe_type', name=_('PoE')),
        FieldSet(
            'mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy',
            name=_('802.1Q Switching'),
            html_id='dot1q-switching',
        ),
        FieldSet(
            'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
            name=_('Wireless')
        ),
    )

    class Meta:
        model = Interface
        fields = [
            'device', 'module', 'vdcs', 'name', 'label', 'type', 'channels', 'channel_id', 'speed', 'duplex',
            'enabled', 'parent', 'bridge', 'lag', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description',
            'poe_mode', 'poe_type', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
            'tx_power', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy',
            'vrf', 'owner', 'tags',
        ]
        widgets = {
            'speed': NumberWithOptions(
                options=InterfaceSpeedChoices
            ),
            'mode': HTMXSelect(hx_target_id='dot1q-switching'),
        }
        labels = {
            'mode': '802.1Q Mode',
        }


class FrontPortForm(FrontPortFormMixin, ModularDeviceComponentForm):
    type = ChoiceField(
        label=_('Type'),
        choices=PortTypeChoices,
    )
    fieldsets = (
        FieldSet(
            'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'mark_connected',
            'description', 'tags',
        ),
    )

    port_mapping_model = PortMapping
    rear_port_model = RearPort

    class Meta:
        model = FrontPort
        fields = [
            'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner',
            'tags',
        ]

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

        # Populate rear port choices
        if device_id := self.data.get('device') or self.initial.get('device'):
            parent_filter = Q(device=device_id)
        else:
            return
        self.fields['rear_ports'].choices = self._get_rear_port_choices(parent_filter, self.instance)

        # Set initial rear port mappings
        if self.instance.pk:
            self.initial['rear_ports'] = [
                f'{mapping.rear_port_id}:{mapping.rear_port_position}'
                for mapping in PortMapping.objects.filter(front_port_id=self.instance.pk)
            ]


class RearPortForm(ModularDeviceComponentForm):
    type = ChoiceField(
        label=_('Type'),
        choices=PortTypeChoices,
    )
    fieldsets = (
        FieldSet(
            'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
        ),
    )

    class Meta:
        model = RearPort
        fields = [
            'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner',
            'tags',
        ]


class ModuleBayForm(ModularDeviceComponentForm):
    module_bay_types = DynamicModelMultipleChoiceField(
        label=_('Module bay types'),
        queryset=ModuleBayType.objects.all(),
        required=False,
    )

    fieldsets = (
        FieldSet('device', 'module', 'name', 'label', 'position', 'enabled', 'description', 'module_bay_types', 'tags'),
    )

    class Meta:
        model = ModuleBay
        fields = [
            'device', 'module', 'name', 'label', 'position', 'enabled', 'description', 'module_bay_types', 'owner',
            'tags',
        ]


class DeviceBayForm(DeviceComponentForm):
    fieldsets = (
        FieldSet('device', 'name', 'label', 'enabled', 'description', 'tags',),
    )

    class Meta:
        model = DeviceBay
        fields = [
            'device', 'name', 'label', 'enabled', 'description', 'owner', 'tags',
        ]


class PopulateDeviceBayForm(forms.Form):
    installed_device = forms.ModelChoiceField(
        queryset=Device.objects.all(),
        label=_('Child Device'),
        help_text=_("Child devices must first be created and assigned to the site and rack of the parent device.")
    )

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

        self.fields['installed_device'].queryset = Device.objects.filter(
            site=device_bay.device.site,
            rack=device_bay.device.rack,
            parent_bay__isnull=True,
            device_type__u_height=0,
            device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
        ).exclude(pk=device_bay.device.pk)


class InventoryItemForm(DeviceComponentForm):
    status = ChoiceField(
        label=_('Status'),
        choices=InventoryItemStatusChoices,
        initial=InventoryItemStatusChoices.STATUS_ACTIVE,
    )
    parent = DynamicModelChoiceField(
        label=_('Parent'),
        queryset=InventoryItem.objects.all(),
        required=False,
        query_params={
            'device_id': '$device'
        }
    )
    role = DynamicModelChoiceField(
        label=_('Role'),
        queryset=InventoryItemRole.objects.all(),
        required=False
    )
    manufacturer = DynamicModelChoiceField(
        label=_('Manufacturer'),
        queryset=Manufacturer.objects.all(),
        required=False
    )

    # Assigned component selectors
    consoleport = DynamicModelChoiceField(
        queryset=ConsolePort.objects.all(),
        required=False,
        query_params={
            'device_id': '$device'
        },
        label=_('Console port')
    )
    consoleserverport = DynamicModelChoiceField(
        queryset=ConsoleServerPort.objects.all(),
        required=False,
        query_params={
            'device_id': '$device'
        },
        label=_('Console server port')
    )
    frontport = DynamicModelChoiceField(
        queryset=FrontPort.objects.all(),
        required=False,
        query_params={
            'device_id': '$device'
        },
        label=_('Front port')
    )
    interface = DynamicModelChoiceField(
        queryset=Interface.objects.all(),
        required=False,
        query_params={
            'device_id': '$device'
        },
        label=_('Interface')
    )
    poweroutlet = DynamicModelChoiceField(
        queryset=PowerOutlet.objects.all(),
        required=False,
        query_params={
            'device_id': '$device'
        },
        label=_('Power outlet')
    )
    powerport = DynamicModelChoiceField(
        queryset=PowerPort.objects.all(),
        required=False,
        query_params={
            'device_id': '$device'
        },
        label=_('Power port')
    )
    rearport = DynamicModelChoiceField(
        queryset=RearPort.objects.all(),
        required=False,
        query_params={
            'device_id': '$device'
        },
        label=_('Rear port')
    )

    fieldsets = (
        FieldSet(
            'device', 'parent', 'name', 'label', 'status', 'role', 'description', 'tags',
            name=_('Inventory Item')
        ),
        FieldSet('manufacturer', 'part_id', 'serial', 'asset_tag', name=_('Hardware')),
        FieldSet(
            TabbedGroups(
                FieldSet('interface', name=_('Interface')),
                FieldSet('consoleport', name=_('Console Port')),
                FieldSet('consoleserverport', name=_('Console Server Port')),
                FieldSet('frontport', name=_('Front Port')),
                FieldSet('rearport', name=_('Rear Port')),
                FieldSet('powerport', name=_('Power Port')),
                FieldSet('poweroutlet', name=_('Power Outlet')),
            ),
            name=_('Component Assignment')
        )
    )

    class Meta:
        model = InventoryItem
        fields = [
            'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
            'status', 'description', 'owner', 'tags',
        ]

    def __init__(self, *args, **kwargs):
        instance = kwargs.get('instance')
        initial = kwargs.get('initial', {}).copy()
        component_type = initial.get('component_type')
        component_id = initial.get('component_id')

        if instance:
            # When editing set the initial value for component selection
            for component_model in ContentType.objects.filter(MODULAR_COMPONENT_MODELS):
                if type(instance.component) is component_model.model_class():
                    initial[component_model.model] = instance.component
                    break
        elif component_type and component_id:
            # When adding the InventoryItem from a component page
            if content_type := ContentType.objects.filter(MODULAR_COMPONENT_MODELS).filter(pk=component_type).first():
                if component := content_type.model_class().objects.filter(pk=component_id).first():
                    initial[content_type.model] = component

        kwargs['initial'] = initial

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

        # Specifically allow editing the device of IntentoryItems
        if self.instance.pk:
            self.fields['device'].disabled = False

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

        # Handle object assignment
        selected_objects = [
            field for field in (
                'consoleport', 'consoleserverport', 'frontport', 'interface', 'poweroutlet', 'powerport', 'rearport'
            ) if self.cleaned_data[field]
        ]
        if len(selected_objects) > 1:
            raise forms.ValidationError(_("An InventoryItem can only be assigned to a single component."))
        if selected_objects:
            self.instance.component = self.cleaned_data[selected_objects[0]]
        else:
            self.instance.component = None


class InventoryItemRoleForm(OrganizationalModelForm):
    fieldsets = (
        FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Inventory Item Role')),
    )

    class Meta:
        model = InventoryItemRole
        fields = [
            'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
        ]


class VirtualDeviceContextForm(TenancyForm, PrimaryModelForm):
    status = ChoiceField(
        label=_('Status'),
        choices=VirtualDeviceContextStatusChoices,
    )
    device = DynamicModelChoiceField(
        label=_('Device'),
        queryset=Device.objects.all(),
        selector=True
    )
    primary_ip4 = DynamicModelChoiceField(
        queryset=IPAddress.objects.all(),
        label=_('Primary IPv4'),
        required=False,
        query_params={
            'device_id': '$device',
            'family': '4',
        }
    )
    primary_ip6 = DynamicModelChoiceField(
        queryset=IPAddress.objects.all(),
        label=_('Primary IPv6'),
        required=False,
        query_params={
            'device_id': '$device',
            'family': '6',
        }
    )

    fieldsets = (
        FieldSet(
            'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags',
            name=_('Virtual Device Context')
        ),
        FieldSet('tenant_group', 'tenant', name=_('Tenancy'))
    )

    class Meta:
        model = VirtualDeviceContext
        fields = [
            'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'owner',
            'comments', 'tags'
        ]


#
# Addressing
#

class MACAddressForm(PrimaryModelForm):
    mac_address = forms.CharField(
        required=True,
        label=_('MAC address')
    )
    interface = DynamicModelChoiceField(
        label=_('Interface'),
        queryset=Interface.objects.all(),
        required=False,
        selector=True,
        context={
            'parent': 'device',
        },
    )
    vminterface = DynamicModelChoiceField(
        label=_('VM Interface'),
        queryset=VMInterface.objects.all(),
        required=False,
        selector=True,
        context={
            'parent': 'virtual_machine',
        },
    )

    fieldsets = (
        FieldSet(
            'mac_address', 'description', 'tags',
        ),
        FieldSet(
            TabbedGroups(
                FieldSet('interface', name=_('Device')),
                FieldSet('vminterface', name=_('Virtual Machine')),
            ),
        ),
    )

    class Meta:
        model = MACAddress
        fields = [
            'mac_address', 'interface', 'vminterface', 'description', 'owner', 'comments', 'tags',
        ]

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

        # Initialize helper selectors
        instance = kwargs.get('instance')
        initial = kwargs.get('initial', {}).copy()
        if instance:
            if type(instance.assigned_object) is Interface:
                initial['interface'] = instance.assigned_object
            elif type(instance.assigned_object) is VMInterface:
                initial['vminterface'] = instance.assigned_object
        kwargs['initial'] = initial

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

        if instance and instance.assigned_object and instance.assigned_object.primary_mac_address:
            if instance.assigned_object.primary_mac_address.pk == instance.pk:
                self.fields['interface'].disabled = True
                self.fields['vminterface'].disabled = True

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

        # Handle object assignment
        selected_objects = [
            field for field in ('interface', 'vminterface') if self.cleaned_data[field]
        ]
        if len(selected_objects) > 1:
            raise forms.ValidationError({
                selected_objects[1]: _("A MAC address can only be assigned to a single object.")
            })
        if selected_objects:
            self.instance.assigned_object = self.cleaned_data[selected_objects[0]]
        else:
            self.instance.assigned_object = None
