import django_filters
from django.contrib.contenttypes.models import ContentType
from django.db.models import Prefetch
from django.db.models.expressions import RawSQL
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django_filters.constants import EMPTY_VALUES

from circuits.models import Provider
from dcim.filtersets import InterfaceFilterSet
from dcim.forms import InterfaceFilterForm
from dcim.models import Device, Interface, Site
from extras.ui.panels import CustomFieldsPanel, TagsPanel
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
from netbox.ui import actions, layout
from netbox.ui.panels import (
    CommentsPanel,
    ContextTablePanel,
    ObjectsTablePanel,
    PluginContentPanel,
    RelatedObjectsPanel,
    TemplatePanel,
)
from netbox.views import generic
from utilities.query import count_related
from utilities.tables import get_table_ordering
from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
from virtualization.filtersets import VMInterfaceFilterSet
from virtualization.forms import VMInterfaceFilterForm
from virtualization.models import VirtualMachine, VMInterface

from . import filtersets, forms, tables
from .choices import PrefixStatusChoices
from .constants import *
from .models import *
from .ui import panels
from .utils import add_available_vlans, add_requested_prefixes, annotate_ip_space

#
# VRFs
#


@register_model_view(VRF, 'list', path='', detail=False)
class VRFListView(generic.ObjectListView):
    queryset = VRF.objects.all()
    filterset = filtersets.VRFFilterSet
    filterset_form = forms.VRFFilterForm
    table = tables.VRFTable


@register_model_view(VRF)
class VRFView(GetRelatedModelsMixin, generic.ObjectView):
    queryset = VRF.objects.all()
    template_name = 'generic/object.html'
    layout = layout.Layout(
        layout.Row(
            layout.Column(
                panels.VRFPanel(),
                TagsPanel(),
                PluginContentPanel('left_page'),
            ),
            layout.Column(
                RelatedObjectsPanel(),
                CustomFieldsPanel(),
                CommentsPanel(),
                PluginContentPanel('right_page'),
            ),
        ),
        layout.Row(
            layout.Column(
                ContextTablePanel('import_targets_table', title=_('Import route targets')),
            ),
            layout.Column(
                ContextTablePanel('export_targets_table', title=_('Export route targets')),
            ),
        ),
        layout.Row(
            layout.Column(
                PluginContentPanel('full_width_page'),
            ),
        ),
    )

    def get_extra_context(self, request, instance):
        import_targets_table = tables.RouteTargetTable(
            instance.import_targets.all(),
            orderable=False
        )
        import_targets_table.configure(request)

        export_targets_table = tables.RouteTargetTable(
            instance.export_targets.all(),
            orderable=False
        )
        export_targets_table.configure(request)

        related_models = self.get_related_models(
            request,
            instance,
            omit=(Interface, VMInterface),
            extra=(
                (
                    Interface.objects.restrict(request.user, 'view').filter(vrf=instance),
                    'vrf_id',
                    _('Device Interfaces')
                ),
                (
                    VMInterface.objects.restrict(request.user, 'view').filter(vrf=instance),
                    'vrf_id',
                    _('VM Interfaces')
                ),
            ),
        )

        return {
            'related_models': related_models,
            'import_targets_table': import_targets_table,
            'export_targets_table': export_targets_table,
        }


@register_model_view(VRF, 'add', detail=False)
@register_model_view(VRF, 'edit')
class VRFEditView(generic.ObjectEditView):
    queryset = VRF.objects.all()
    form = forms.VRFForm


@register_model_view(VRF, 'delete')
class VRFDeleteView(generic.ObjectDeleteView):
    queryset = VRF.objects.all()


@register_model_view(VRF, 'bulk_import', path='import', detail=False)
class VRFBulkImportView(generic.BulkImportView):
    queryset = VRF.objects.all()
    model_form = forms.VRFImportForm


@register_model_view(VRF, 'bulk_edit', path='edit', detail=False)
class VRFBulkEditView(generic.BulkEditView):
    queryset = VRF.objects.all()
    filterset = filtersets.VRFFilterSet
    table = tables.VRFTable
    form = forms.VRFBulkEditForm


@register_model_view(VRF, 'bulk_rename', path='rename', detail=False)
class VRFBulkRenameView(generic.BulkRenameView):
    queryset = VRF.objects.all()
    filterset = filtersets.VRFFilterSet


@register_model_view(VRF, 'bulk_delete', path='delete', detail=False)
class VRFBulkDeleteView(generic.BulkDeleteView):
    queryset = VRF.objects.all()
    filterset = filtersets.VRFFilterSet
    table = tables.VRFTable


#
# Route targets
#

@register_model_view(RouteTarget, 'list', path='', detail=False)
class RouteTargetListView(generic.ObjectListView):
    queryset = RouteTarget.objects.all()
    filterset = filtersets.RouteTargetFilterSet
    filterset_form = forms.RouteTargetFilterForm
    table = tables.RouteTargetTable


@register_model_view(RouteTarget)
class RouteTargetView(generic.ObjectView):
    queryset = RouteTarget.objects.all()
    template_name = 'generic/object.html'
    layout = layout.Layout(
        layout.Row(
            layout.Column(
                panels.RouteTargetPanel(),
                TagsPanel(),
                PluginContentPanel('left_page'),
            ),
            layout.Column(
                CustomFieldsPanel(),
                CommentsPanel(),
                PluginContentPanel('right_page'),
            ),
        ),
        layout.Row(
            layout.Column(
                ObjectsTablePanel(
                    'ipam.vrf',
                    filters={'import_target_id': lambda ctx: ctx['object'].pk},
                    title=_('Importing VRFs'),
                ),
            ),
            layout.Column(
                ObjectsTablePanel(
                    'ipam.vrf',
                    filters={'export_target_id': lambda ctx: ctx['object'].pk},
                    title=_('Exporting VRFs'),
                ),
            ),
        ),
        layout.Row(
            layout.Column(
                ObjectsTablePanel(
                    'vpn.l2vpn',
                    filters={'import_target_id': lambda ctx: ctx['object'].pk},
                    title=_('Importing L2VPNs'),
                ),
            ),
            layout.Column(
                ObjectsTablePanel(
                    'vpn.l2vpn',
                    filters={'export_target_id': lambda ctx: ctx['object'].pk},
                    title=_('Exporting L2VPNs'),
                ),
            ),
        ),
        layout.Row(
            layout.Column(
                PluginContentPanel('full_width_page'),
            ),
        ),
    )


@register_model_view(RouteTarget, 'add', detail=False)
@register_model_view(RouteTarget, 'edit')
class RouteTargetEditView(generic.ObjectEditView):
    queryset = RouteTarget.objects.all()
    form = forms.RouteTargetForm


@register_model_view(RouteTarget, 'delete')
class RouteTargetDeleteView(generic.ObjectDeleteView):
    queryset = RouteTarget.objects.all()


@register_model_view(RouteTarget, 'bulk_import', path='import', detail=False)
class RouteTargetBulkImportView(generic.BulkImportView):
    queryset = RouteTarget.objects.all()
    model_form = forms.RouteTargetImportForm


@register_model_view(RouteTarget, 'bulk_edit', path='edit', detail=False)
class RouteTargetBulkEditView(generic.BulkEditView):
    queryset = RouteTarget.objects.all()
    filterset = filtersets.RouteTargetFilterSet
    table = tables.RouteTargetTable
    form = forms.RouteTargetBulkEditForm


@register_model_view(RouteTarget, 'bulk_rename', path='rename', detail=False)
class RouteTargetBulkRenameView(generic.BulkRenameView):
    queryset = RouteTarget.objects.all()
    filterset = filtersets.RouteTargetFilterSet


@register_model_view(RouteTarget, 'bulk_delete', path='delete', detail=False)
class RouteTargetBulkDeleteView(generic.BulkDeleteView):
    queryset = RouteTarget.objects.all()
    filterset = filtersets.RouteTargetFilterSet
    table = tables.RouteTargetTable


#
# RIRs
#

@register_model_view(RIR, 'list', path='', detail=False)
class RIRListView(generic.ObjectListView):
    queryset = RIR.objects.annotate(
        aggregate_count=count_related(Aggregate, 'rir')
    )
    filterset = filtersets.RIRFilterSet
    filterset_form = forms.RIRFilterForm
    table = tables.RIRTable


@register_model_view(RIR)
class RIRView(GetRelatedModelsMixin, generic.ObjectView):
    queryset = RIR.objects.all()
    layout = layout.SimpleLayout(
        left_panels=[
            panels.RIRPanel(),
            TagsPanel(),
        ],
        right_panels=[
            RelatedObjectsPanel(),
            CommentsPanel(),
            CustomFieldsPanel(),
        ],
    )

    def get_extra_context(self, request, instance):
        return {
            'related_models': self.get_related_models(request, instance),
        }


@register_model_view(RIR, 'add', detail=False)
@register_model_view(RIR, 'edit')
class RIREditView(generic.ObjectEditView):
    queryset = RIR.objects.all()
    form = forms.RIRForm


@register_model_view(RIR, 'delete')
class RIRDeleteView(generic.ObjectDeleteView):
    queryset = RIR.objects.all()


@register_model_view(RIR, 'bulk_import', path='import', detail=False)
class RIRBulkImportView(generic.BulkImportView):
    queryset = RIR.objects.all()
    model_form = forms.RIRImportForm


@register_model_view(RIR, 'bulk_edit', path='edit', detail=False)
class RIRBulkEditView(generic.BulkEditView):
    queryset = RIR.objects.annotate(
        aggregate_count=count_related(Aggregate, 'rir')
    )
    filterset = filtersets.RIRFilterSet
    table = tables.RIRTable
    form = forms.RIRBulkEditForm


@register_model_view(RIR, 'bulk_rename', path='rename', detail=False)
class RIRBulkRenameView(generic.BulkRenameView):
    queryset = RIR.objects.all()
    filterset = filtersets.RIRFilterSet


@register_model_view(RIR, 'bulk_delete', path='delete', detail=False)
class RIRBulkDeleteView(generic.BulkDeleteView):
    queryset = RIR.objects.annotate(
        aggregate_count=count_related(Aggregate, 'rir')
    )
    filterset = filtersets.RIRFilterSet
    table = tables.RIRTable


#
# ASN ranges
#

@register_model_view(ASNRange, 'list', path='', detail=False)
class ASNRangeListView(generic.ObjectListView):
    queryset = ASNRange.objects.annotate_asn_counts()
    filterset = filtersets.ASNRangeFilterSet
    filterset_form = forms.ASNRangeFilterForm
    table = tables.ASNRangeTable


@register_model_view(ASNRange)
class ASNRangeView(generic.ObjectView):
    queryset = ASNRange.objects.all()
    layout = layout.SimpleLayout(
        left_panels=[
            panels.ASNRangePanel(),
            TagsPanel(),
        ],
        right_panels=[
            CommentsPanel(),
            CustomFieldsPanel(),
        ],
    )


@register_model_view(ASNRange, 'asns')
class ASNRangeASNsView(generic.ObjectChildrenView):
    queryset = ASNRange.objects.all()
    child_model = ASN
    table = tables.ASNTable
    filterset = filtersets.ASNFilterSet
    filterset_form = forms.ASNFilterForm
    tab = ViewTab(
        label=_('ASNs'),
        badge=lambda x: x.get_child_asns().count(),
        permission='ipam.view_asn',
        weight=500
    )

    def get_children(self, request, parent):
        return parent.get_child_asns().restrict(request.user, 'view').annotate(
            site_count=count_related(Site, 'asns'),
            provider_count=count_related(Provider, 'asns')
        )


@register_model_view(ASNRange, 'add', detail=False)
@register_model_view(ASNRange, 'edit')
class ASNRangeEditView(generic.ObjectEditView):
    queryset = ASNRange.objects.all()
    form = forms.ASNRangeForm


@register_model_view(ASNRange, 'delete')
class ASNRangeDeleteView(generic.ObjectDeleteView):
    queryset = ASNRange.objects.all()


@register_model_view(ASNRange, 'bulk_import', path='import', detail=False)
class ASNRangeBulkImportView(generic.BulkImportView):
    queryset = ASNRange.objects.all()
    model_form = forms.ASNRangeImportForm


@register_model_view(ASNRange, 'bulk_edit', path='edit', detail=False)
class ASNRangeBulkEditView(generic.BulkEditView):
    queryset = ASNRange.objects.annotate_asn_counts()
    filterset = filtersets.ASNRangeFilterSet
    table = tables.ASNRangeTable
    form = forms.ASNRangeBulkEditForm


@register_model_view(ASNRange, 'bulk_rename', path='rename', detail=False)
class ASNRangeBulkRenameView(generic.BulkRenameView):
    queryset = ASNRange.objects.all()
    filterset = filtersets.ASNRangeFilterSet


@register_model_view(ASNRange, 'bulk_delete', path='delete', detail=False)
class ASNRangeBulkDeleteView(generic.BulkDeleteView):
    queryset = ASNRange.objects.annotate_asn_counts()
    filterset = filtersets.ASNRangeFilterSet
    table = tables.ASNRangeTable


#
# ASNs
#

@register_model_view(ASN, 'list', path='', detail=False)
class ASNListView(generic.ObjectListView):
    queryset = ASN.objects.annotate(
        site_count=count_related(Site, 'asns'),
        provider_count=count_related(Provider, 'asns')
    )
    filterset = filtersets.ASNFilterSet
    filterset_form = forms.ASNFilterForm
    table = tables.ASNTable


@register_model_view(ASN)
class ASNView(GetRelatedModelsMixin, generic.ObjectView):
    queryset = ASN.objects.all()
    layout = layout.SimpleLayout(
        left_panels=[
            panels.ASNPanel(),
            TagsPanel(),
        ],
        right_panels=[
            RelatedObjectsPanel(),
            CustomFieldsPanel(),
            CommentsPanel(),
        ],
    )

    def get_extra_context(self, request, instance):
        return {
            'related_models': self.get_related_models(
                request,
                instance,
                extra=(
                    (Site.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'),
                    (Provider.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'),
                ),
            ),
        }


@register_model_view(ASN, 'add', detail=False)
@register_model_view(ASN, 'edit')
class ASNEditView(generic.ObjectEditView):
    queryset = ASN.objects.all()
    form = forms.ASNForm


@register_model_view(ASN, 'delete')
class ASNDeleteView(generic.ObjectDeleteView):
    queryset = ASN.objects.all()


@register_model_view(ASN, 'bulk_import', path='import', detail=False)
class ASNBulkImportView(generic.BulkImportView):
    queryset = ASN.objects.all()
    model_form = forms.ASNImportForm


@register_model_view(ASN, 'bulk_edit', path='edit', detail=False)
class ASNBulkEditView(generic.BulkEditView):
    queryset = ASN.objects.annotate(
        site_count=count_related(Site, 'asns')
    )
    filterset = filtersets.ASNFilterSet
    table = tables.ASNTable
    form = forms.ASNBulkEditForm


@register_model_view(ASN, 'bulk_rename', path='rename', detail=False)
class ASNBulkRenameView(generic.BulkRenameView):
    queryset = ASN.objects.all()
    filterset = filtersets.ASNFilterSet


@register_model_view(ASN, 'bulk_delete', path='delete', detail=False)
class ASNBulkDeleteView(generic.BulkDeleteView):
    queryset = ASN.objects.annotate(
        site_count=count_related(Site, 'asns')
    )
    filterset = filtersets.ASNFilterSet
    table = tables.ASNTable


#
# Aggregates
#

@register_model_view(Aggregate, 'list', path='', detail=False)
class AggregateListView(generic.ObjectListView):
    queryset = Aggregate.objects.annotate(
        child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
    )
    filterset = filtersets.AggregateFilterSet
    filterset_form = forms.AggregateFilterForm
    table = tables.AggregateTable
    actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)


@register_model_view(Aggregate)
class AggregateView(generic.ObjectView):
    queryset = Aggregate.objects.all()
    layout = layout.SimpleLayout(
        left_panels=[
            panels.AggregatePanel(),
        ],
        right_panels=[
            CustomFieldsPanel(),
            TagsPanel(),
            CommentsPanel(),
        ],
    )


class ChildAvailabilityMixin:
    """
    Mixin for ObjectChildrenView subclasses that render synthetic "available" rows
    (available IP space, prefixes, or VLANs) and must suppress them when the request
    activates a child object filter, so objects excluded by the filter are not
    misrepresented as available space.
    """

    def _get_detection_filterset(self, request):
        """
        Return the bound FilterSet used to evaluate the request, or None if the view declares no
        filterset. ObjectChildrenView.get() has already built one and validated its form while
        resolving the child queryset, so reuse it rather than paying for a second FilterSet:
        get_filters() regenerates every dynamic lookup variant and NetBoxModelFilterSet.__init__
        queries the custom fields for the model. Build one only for direct calls where get() has
        not run.
        """
        if self.filterset_instance is not None:
            return self.filterset_instance
        if self.filterset is None:
            return None

        return self.filterset(request.GET, request=request)

    def _has_active_child_filters(self, request):
        """
        Return True if the request supplies a valid, non-empty value for any filter declared by
        the view's filterset. Saved filters are expanded during filterset instantiation and
        dynamic custom field filters are registered on the instance, so both are detected.
        Permission constraints and parent scoping never appear in the request, so they cannot
        affect the result. The result is memoized because a single request evaluates it from
        both prep_table_data and get_extra_context.
        """
        if hasattr(self, '_active_child_filters'):
            return self._active_child_filters

        self._active_child_filters = False

        # An empty request cannot activate a filter, so skip validating a form for nothing.
        if not request.GET:
            return False

        filterset = self._get_detection_filterset(request)
        if filterset is None:
            return False

        # A non-empty cleaned value means the request activated a declared filter. Emptiness
        # follows each filter's own semantics, so absent multi-value fields stay inactive.
        # This is a no-op for a reused FilterSet: .qs validated the form to build the queryset.
        filterset.form.is_valid()
        for name, value in filterset.form.cleaned_data.items():
            if isinstance(filterset.filters[name], django_filters.MultipleChoiceFilter):
                if value:  # mirrors MultipleChoiceFilter.filter()
                    self._active_child_filters = True
                    break
            elif value not in EMPTY_VALUES:  # mirrors Filter.filter()
                self._active_child_filters = True
                break

        return self._active_child_filters


@register_model_view(Aggregate, 'prefixes')
class AggregatePrefixesView(ChildAvailabilityMixin, generic.ObjectChildrenView):
    queryset = Aggregate.objects.all()
    child_model = Prefix
    table = tables.PrefixTable
    filterset = filtersets.PrefixFilterSet
    filterset_form = forms.PrefixFilterForm
    template_name = 'ipam/aggregate/prefixes.html'
    tab = ViewTab(
        label=_('Prefixes'),
        badge=lambda x: x.get_child_prefixes().count(),
        permission='ipam.view_prefix',
        weight=500
    )

    def get_children(self, request, parent):
        return Prefix.objects.restrict(request.user, 'view').filter(
            prefix__net_contained_or_equal=str(parent.prefix)
        ).prefetch_related('scope', 'role', 'tenant', 'tenant__group', 'vlan')

    def prep_table_data(self, request, queryset, parent):
        # Determine whether to show assigned prefixes, available prefixes, or both
        show_available = bool(request.GET.get('show_available', 'true') == 'true')
        show_assigned = bool(request.GET.get('show_assigned', 'true') == 'true')

        if show_available and self._has_active_child_filters(request):
            show_available = False

        return add_requested_prefixes(parent.prefix, queryset, show_available, show_assigned)

    def get_extra_context(self, request, instance):
        show_available = (
            bool(request.GET.get('show_available', 'true') == 'true') and
            not self._has_active_child_filters(request)
        )

        return {
            'bulk_querystring': f'within={instance.prefix}',
            'first_available_prefix': instance.get_first_available_prefix(),
            'show_available': show_available,
            'show_assigned': bool(request.GET.get('show_assigned', 'true') == 'true'),
        }


@register_model_view(Aggregate, 'add', detail=False)
@register_model_view(Aggregate, 'edit')
class AggregateEditView(generic.ObjectEditView):
    queryset = Aggregate.objects.all()
    form = forms.AggregateForm


@register_model_view(Aggregate, 'delete')
class AggregateDeleteView(generic.ObjectDeleteView):
    queryset = Aggregate.objects.all()


@register_model_view(Aggregate, 'bulk_import', path='import', detail=False)
class AggregateBulkImportView(generic.BulkImportView):
    queryset = Aggregate.objects.all()
    model_form = forms.AggregateImportForm


@register_model_view(Aggregate, 'bulk_edit', path='edit', detail=False)
class AggregateBulkEditView(generic.BulkEditView):
    queryset = Aggregate.objects.annotate(
        child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
    )
    filterset = filtersets.AggregateFilterSet
    table = tables.AggregateTable
    form = forms.AggregateBulkEditForm


@register_model_view(Aggregate, 'bulk_delete', path='delete', detail=False)
class AggregateBulkDeleteView(generic.BulkDeleteView):
    queryset = Aggregate.objects.annotate(
        child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
    )
    filterset = filtersets.AggregateFilterSet
    table = tables.AggregateTable


#
# Prefix/VLAN roles
#

@register_model_view(Role, 'list', path='', detail=False)
class RoleListView(generic.ObjectListView):
    queryset = Role.objects.annotate(
        prefix_count=count_related(Prefix, 'role'),
        iprange_count=count_related(IPRange, 'role'),
        vlan_count=count_related(VLAN, 'role'),
        asn_count=count_related(ASN, 'role')
    )
    filterset = filtersets.RoleFilterSet
    filterset_form = forms.RoleFilterForm
    table = tables.RoleTable


@register_model_view(Role)
class RoleView(GetRelatedModelsMixin, generic.ObjectView):
    queryset = Role.objects.all()
    layout = layout.SimpleLayout(
        left_panels=[
            panels.RolePanel(),
            TagsPanel(),
        ],
        right_panels=[
            RelatedObjectsPanel(),
            CommentsPanel(),
            CustomFieldsPanel(),
        ],
    )

    def get_extra_context(self, request, instance):
        return {
            'related_models': self.get_related_models(request, instance),
        }


@register_model_view(Role, 'add', detail=False)
@register_model_view(Role, 'edit')
class RoleEditView(generic.ObjectEditView):
    queryset = Role.objects.all()
    form = forms.RoleForm


@register_model_view(Role, 'delete')
class RoleDeleteView(generic.ObjectDeleteView):
    queryset = Role.objects.all()


@register_model_view(Role, 'bulk_import', path='import', detail=False)
class RoleBulkImportView(generic.BulkImportView):
    queryset = Role.objects.all()
    model_form = forms.RoleImportForm


@register_model_view(Role, 'bulk_edit', path='edit', detail=False)
class RoleBulkEditView(generic.BulkEditView):
    queryset = Role.objects.all()
    filterset = filtersets.RoleFilterSet
    table = tables.RoleTable
    form = forms.RoleBulkEditForm


@register_model_view(Role, 'bulk_rename', path='rename', detail=False)
class RoleBulkRenameView(generic.BulkRenameView):
    queryset = Role.objects.all()
    filterset = filtersets.RoleFilterSet


@register_model_view(Role, 'bulk_delete', path='delete', detail=False)
class RoleBulkDeleteView(generic.BulkDeleteView):
    queryset = Role.objects.all()
    filterset = filtersets.RoleFilterSet
    table = tables.RoleTable


#
# Prefixes
#

@register_model_view(Prefix, 'list', path='', detail=False)
class PrefixListView(generic.ObjectListView):
    queryset = Prefix.objects.all()
    filterset = filtersets.PrefixFilterSet
    filterset_form = forms.PrefixFilterForm
    table = tables.PrefixTable
    template_name = 'ipam/prefix_list.html'
    actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)


@register_model_view(Prefix)
class PrefixView(generic.ObjectView):
    queryset = Prefix.objects.all()
    layout = layout.SimpleLayout(
        left_panels=[
            panels.PrefixPanel(),
        ],
        right_panels=[
            TemplatePanel('ipam/panels/prefix_addressing.html'),
            CustomFieldsPanel(),
            TagsPanel(),
            CommentsPanel(),
        ],
        bottom_panels=[
            ContextTablePanel('duplicate_prefix_table', title=_('Duplicate prefixes')),
            ContextTablePanel('parent_prefix_table', title=_('Parent prefixes')),
        ],
    )

    def get_extra_context(self, request, instance):
        # Parent prefixes table
        parent_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
            Q(vrf=instance.vrf) | Q(vrf__isnull=True, status=PrefixStatusChoices.STATUS_CONTAINER)
        ).filter(
            prefix__net_contains=str(instance.prefix)
        ).prefetch_related(
            'scope', 'role', 'tenant', 'vlan',
        )
        parent_prefix_table = tables.PrefixTable(
            list(parent_prefixes),
            exclude=('vrf', 'utilization'),
            orderable=False
        )
        parent_prefix_table.configure(request)

        # Duplicate prefixes table
        duplicate_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
            vrf=instance.vrf, prefix=str(instance.prefix)
        ).exclude(
            pk=instance.pk
        ).prefetch_related(
            'scope', 'role', 'tenant', 'vlan',
        )
        duplicate_prefix_table = tables.PrefixTable(
            list(duplicate_prefixes),
            exclude=('vrf', 'utilization'),
            orderable=False
        )
        duplicate_prefix_table.configure(request)

        context = {
            'parent_prefix_table': parent_prefix_table,
        }
        if duplicate_prefixes.exists():
            context['duplicate_prefix_table'] = duplicate_prefix_table
        return context


@register_model_view(Prefix, 'prefixes')
class PrefixPrefixesView(ChildAvailabilityMixin, generic.ObjectChildrenView):
    queryset = Prefix.objects.all()
    child_model = Prefix
    table = tables.PrefixTable
    filterset = filtersets.PrefixFilterSet
    filterset_form = forms.PrefixFilterForm
    template_name = 'ipam/prefix/prefixes.html'
    tab = ViewTab(
        label=_('Child Prefixes'),
        badge=lambda x: x.get_child_prefixes().count(),
        permission='ipam.view_prefix',
        weight=500
    )

    def get_children(self, request, parent):
        return parent.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
            'scope', 'vrf', 'vlan', 'role', 'tenant', 'tenant__group'
        )

    def prep_table_data(self, request, queryset, parent):
        # Determine whether to show assigned prefixes, available prefixes, or both
        show_available = bool(request.GET.get('show_available', 'true') == 'true')
        show_assigned = bool(request.GET.get('show_assigned', 'true') == 'true')

        if show_available and self._has_active_child_filters(request):
            show_available = False

        return add_requested_prefixes(parent.prefix, queryset, show_available, show_assigned)

    def get_extra_context(self, request, instance):
        show_available = (
            bool(request.GET.get('show_available', 'true') == 'true') and
            not self._has_active_child_filters(request)
        )

        return {
            'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&within={instance.prefix}",
            'first_available_prefix': instance.get_first_available_prefix(),
            'show_available': show_available,
            'show_assigned': bool(request.GET.get('show_assigned', 'true') == 'true'),
        }


@register_model_view(Prefix, 'ipranges', path='ip-ranges')
class PrefixIPRangesView(generic.ObjectChildrenView):
    queryset = Prefix.objects.all()
    child_model = IPRange
    table = tables.IPRangeTable
    filterset = filtersets.IPRangeFilterSet
    filterset_form = forms.IPRangeFilterForm
    template_name = 'ipam/prefix/ip_ranges.html'
    tab = ViewTab(
        label=_('Child Ranges'),
        badge=lambda x: x.get_child_ranges().count(),
        permission='ipam.view_iprange',
        weight=600
    )

    def get_children(self, request, parent):
        return parent.get_child_ranges().restrict(request.user, 'view').prefetch_related(
            'tenant__group',
        )

    def get_extra_context(self, request, instance):
        return {
            'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}",
            'first_available_ip': instance.get_first_available_ip(),
        }


@register_model_view(Prefix, 'ipaddresses', path='ip-addresses')
class PrefixIPAddressesView(ChildAvailabilityMixin, generic.ObjectChildrenView):
    queryset = Prefix.objects.all()
    child_model = IPAddress
    table = tables.AnnotatedIPAddressTable
    filterset = filtersets.IPAddressFilterSet
    filterset_form = forms.IPAddressFilterForm
    template_name = 'ipam/prefix/ip_addresses.html'
    tab = ViewTab(
        label=_('IP Addresses'),
        badge=lambda x: x.get_child_ips().count(),
        permission='ipam.view_ipaddress',
        weight=700
    )

    def get_children(self, request, parent):
        return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group')

    def prep_table_data(self, request, queryset, parent):
        # Ordering is checked first: it reads request.GET directly, so a sorted request never
        # builds the detection filterset.
        if not get_table_ordering(request, self.table) and not self._has_active_child_filters(request):
            ip_ranges = parent.get_child_ranges(mark_populated=True).restrict(request.user, 'view')
            return annotate_ip_space(parent, ip_addresses=queryset, ip_ranges=ip_ranges)

        return super().prep_table_data(request, queryset, parent)

    def get_extra_context(self, request, instance):
        return {
            'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}",
            'first_available_ip': instance.get_first_available_ip(),
        }


@register_model_view(Prefix, 'add', detail=False)
@register_model_view(Prefix, 'edit')
class PrefixEditView(generic.ObjectEditView):
    queryset = Prefix.objects.all()
    form = forms.PrefixForm
    template_name = 'ipam/prefix_edit.html'


@register_model_view(Prefix, 'delete')
class PrefixDeleteView(generic.ObjectDeleteView):
    queryset = Prefix.objects.all()


@register_model_view(Prefix, 'bulk_add', path='bulk-add', detail=False)
class PrefixBulkCreateView(generic.BulkCreateView):
    queryset = Prefix.objects.all()
    form = forms.IPNetworkBulkCreateForm
    model_form = forms.PrefixBulkAddForm
    pattern_target = 'prefix'
    template_name = 'ipam/prefix_bulk_add.html'


@register_model_view(Prefix, 'bulk_import', path='import', detail=False)
class PrefixBulkImportView(generic.BulkImportView):
    queryset = Prefix.objects.all()
    model_form = forms.PrefixImportForm


@register_model_view(Prefix, 'bulk_edit', path='edit', detail=False)
class PrefixBulkEditView(generic.BulkEditView):
    queryset = Prefix.objects.prefetch_related('vrf__tenant')
    filterset = filtersets.PrefixFilterSet
    table = tables.PrefixTable
    form = forms.PrefixBulkEditForm


@register_model_view(Prefix, 'bulk_delete', path='delete', detail=False)
class PrefixBulkDeleteView(generic.BulkDeleteView):
    queryset = Prefix.objects.prefetch_related('vrf__tenant')
    filterset = filtersets.PrefixFilterSet
    table = tables.PrefixTable


#
# IP Ranges
#

@register_model_view(IPRange, 'list', path='', detail=False)
class IPRangeListView(generic.ObjectListView):
    queryset = IPRange.objects.all()
    filterset = filtersets.IPRangeFilterSet
    filterset_form = forms.IPRangeFilterForm
    table = tables.IPRangeTable


@register_model_view(IPRange)
class IPRangeView(generic.ObjectView):
    queryset = IPRange.objects.all()
    layout = layout.SimpleLayout(
        left_panels=[
            panels.IPRangePanel(),
        ],
        right_panels=[
            TagsPanel(),
            CustomFieldsPanel(),
            CommentsPanel(),
        ],
        bottom_panels=[
            ContextTablePanel('parent_prefixes_table', title=_('Parent prefixes')),
        ],
    )

    def get_extra_context(self, request, instance):

        # Parent prefixes table
        parent_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
            Q(prefix__net_contains_or_equals=str(instance.start_address.ip)),
            Q(prefix__net_contains_or_equals=str(instance.end_address.ip)),
            vrf=instance.vrf
        ).prefetch_related(
            'scope', 'role', 'tenant', 'vlan', 'role'
        )
        parent_prefixes_table = tables.PrefixTable(
            list(parent_prefixes),
            exclude=('vrf', 'utilization'),
            orderable=False
        )
        parent_prefixes_table.configure(request)

        return {
            'parent_prefixes_table': parent_prefixes_table,
        }


@register_model_view(IPRange, 'ipaddresses', path='ip-addresses')
class IPRangeIPAddressesView(generic.ObjectChildrenView):
    queryset = IPRange.objects.all()
    child_model = IPAddress
    table = tables.IPAddressTable
    filterset = filtersets.IPAddressFilterSet
    filterset_form = forms.IPRangeFilterForm
    template_name = 'ipam/iprange/ip_addresses.html'
    tab = ViewTab(
        label=_('IP Addresses'),
        badge=lambda x: x.get_child_ips().count(),
        permission='ipam.view_ipaddress',
        weight=500
    )

    def get_children(self, request, parent):
        return parent.get_child_ips().restrict(request.user, 'view')


@register_model_view(IPRange, 'add', detail=False)
@register_model_view(IPRange, 'edit')
class IPRangeEditView(generic.ObjectEditView):
    queryset = IPRange.objects.all()
    form = forms.IPRangeForm


@register_model_view(IPRange, 'delete')
class IPRangeDeleteView(generic.ObjectDeleteView):
    queryset = IPRange.objects.all()


@register_model_view(IPRange, 'bulk_import', path='import', detail=False)
class IPRangeBulkImportView(generic.BulkImportView):
    queryset = IPRange.objects.all()
    model_form = forms.IPRangeImportForm


@register_model_view(IPRange, 'bulk_edit', path='edit', detail=False)
class IPRangeBulkEditView(generic.BulkEditView):
    queryset = IPRange.objects.all()
    filterset = filtersets.IPRangeFilterSet
    table = tables.IPRangeTable
    form = forms.IPRangeBulkEditForm


@register_model_view(IPRange, 'bulk_rename', path='rename', detail=False)
class IPRangeBulkRenameView(generic.BulkRenameView):
    queryset = IPRange.objects.all()
    filterset = filtersets.IPRangeFilterSet


@register_model_view(IPRange, 'bulk_delete', path='delete', detail=False)
class IPRangeBulkDeleteView(generic.BulkDeleteView):
    queryset = IPRange.objects.all()
    filterset = filtersets.IPRangeFilterSet
    table = tables.IPRangeTable


#
# IP addresses
#

@register_model_view(IPAddress, 'list', path='', detail=False)
class IPAddressListView(generic.ObjectListView):
    queryset = IPAddress.objects.all()
    filterset = filtersets.IPAddressFilterSet
    filterset_form = forms.IPAddressFilterForm
    table = tables.IPAddressTable
    actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)


@register_model_view(IPAddress)
class IPAddressView(generic.ObjectView):
    queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
    layout = layout.SimpleLayout(
        left_panels=[
            panels.IPAddressPanel(),
            TagsPanel(),
            CustomFieldsPanel(),
            CommentsPanel(),
        ],
        right_panels=[
            ContextTablePanel('parent_prefixes_table', title=_('Parent prefixes')),
            ContextTablePanel('duplicate_ips_table', title=_('Duplicate IPs')),
            ObjectsTablePanel(
                'ipam.service',
                filters={'ip_address_id': lambda ctx: ctx['object'].pk},
                title=_('Application services'),
            ),
        ],
    )

    def get_extra_context(self, request, instance):
        # Parent prefixes table
        parent_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
            vrf=instance.vrf,
            prefix__net_contains_or_equals=str(instance.address.ip)
        ).prefetch_related(
            'scope', 'role'
        )
        parent_prefixes_table = tables.PrefixTable(
            list(parent_prefixes),
            exclude=('vrf', 'utilization'),
            orderable=False
        )
        parent_prefixes_table.configure(request)

        # Duplicate IPs table
        duplicate_ips = IPAddress.objects.restrict(request.user, 'view').filter(
            vrf=instance.vrf,
            address=str(instance.address)
        ).exclude(
            pk=instance.pk
        ).prefetch_related(
            'nat_inside'
        )
        # Exclude anycast IPs if this IP is anycast
        if instance.role == IPAddressRoleChoices.ROLE_ANYCAST:
            duplicate_ips = duplicate_ips.exclude(role=IPAddressRoleChoices.ROLE_ANYCAST)
        # Limit to a maximum of 10 duplicates displayed here
        duplicate_ips_table = tables.IPAddressTable(duplicate_ips[:10], orderable=False)
        duplicate_ips_table.configure(request)

        context = {
            'parent_prefixes_table': parent_prefixes_table,
        }
        if duplicate_ips.exists():
            context['duplicate_ips_table'] = duplicate_ips_table
        return context


@register_model_view(IPAddress, 'add', detail=False)
@register_model_view(IPAddress, 'edit')
class IPAddressEditView(generic.ObjectEditView):
    queryset = IPAddress.objects.all()
    form = forms.IPAddressForm
    template_name = 'ipam/ipaddress_edit.html'

    def alter_object(self, obj, request, url_args, url_kwargs):

        if 'interface' in request.GET:
            try:
                obj.assigned_object = Interface.objects.get(pk=request.GET['interface'])
            except (ValueError, Interface.DoesNotExist):
                pass

        elif 'vminterface' in request.GET:
            try:
                obj.assigned_object = VMInterface.objects.get(pk=request.GET['vminterface'])
            except (ValueError, VMInterface.DoesNotExist):
                pass

        elif 'fhrpgroup' in request.GET:
            try:
                obj.assigned_object = FHRPGroup.objects.get(pk=request.GET['fhrpgroup'])
            except (ValueError, FHRPGroup.DoesNotExist):
                pass

        return obj

    def get_extra_addanother_params(self, request):
        if 'interface' in request.GET:
            return {'interface': request.GET['interface']}
        if 'vminterface' in request.GET:
            return {'vminterface': request.GET['vminterface']}
        return {}


# TODO: Standardize or remove this view
@register_model_view(IPAddress, 'assign', path='assign', detail=False)
class IPAddressAssignView(generic.ObjectView):
    """
    Search for IPAddresses to be assigned to an Interface.
    """
    queryset = IPAddress.objects.all()

    def dispatch(self, request, *args, **kwargs):

        # Redirect user if an interface has not been provided
        if 'interface' not in request.GET and 'vminterface' not in request.GET:
            return redirect('ipam:ipaddress_add')

        return super().dispatch(request, *args, **kwargs)

    def get(self, request):
        form = forms.IPAddressAssignForm()

        return render(request, 'ipam/ipaddress_assign.html', {
            'form': form,
            'return_url': request.GET.get('return_url', ''),
        })

    def post(self, request):
        form = forms.IPAddressAssignForm(request.POST)
        table = None

        if form.is_valid():
            addresses = self.queryset.prefetch_related('vrf', 'tenant')
            # Limit to 100 results
            addresses = filtersets.IPAddressFilterSet(request.POST, addresses).qs[:100]
            table = tables.IPAddressAssignTable(addresses)
            table.configure(request)

        return render(request, 'ipam/ipaddress_assign.html', {
            'form': form,
            'table': table,
            'return_url': request.GET.get('return_url'),
        })


@register_model_view(IPAddress, 'delete')
class IPAddressDeleteView(generic.ObjectDeleteView):
    queryset = IPAddress.objects.all()


@register_model_view(IPAddress, 'bulk_add', path='bulk-add', detail=False)
class IPAddressBulkCreateView(generic.BulkCreateView):
    queryset = IPAddress.objects.all()
    form = forms.IPNetworkBulkCreateForm
    model_form = forms.IPAddressBulkAddForm
    pattern_target = 'address'
    template_name = 'ipam/ipaddress_bulk_add.html'


@register_model_view(IPAddress, 'bulk_import', path='import', detail=False)
class IPAddressBulkImportView(generic.BulkImportView):
    queryset = IPAddress.objects.all()
    model_form = forms.IPAddressImportForm


@register_model_view(IPAddress, 'bulk_edit', path='edit', detail=False)
class IPAddressBulkEditView(generic.BulkEditView):
    queryset = IPAddress.objects.prefetch_related('vrf__tenant')
    filterset = filtersets.IPAddressFilterSet
    table = tables.IPAddressTable
    form = forms.IPAddressBulkEditForm


@register_model_view(IPAddress, 'bulk_delete', path='delete', detail=False)
class IPAddressBulkDeleteView(generic.BulkDeleteView):
    queryset = IPAddress.objects.prefetch_related('vrf__tenant')
    filterset = filtersets.IPAddressFilterSet
    table = tables.IPAddressTable


@register_model_view(IPAddress, 'related_ips', path='related-ip-addresses')
class IPAddressRelatedIPsView(generic.ObjectChildrenView):
    queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
    child_model = IPAddress
    table = tables.IPAddressTable
    filterset = filtersets.IPAddressFilterSet
    filterset_form = forms.IPAddressFilterForm
    tab = ViewTab(
        label=_('Related IPs'),
        badge=lambda x: x.get_related_ips().count(),
        weight=500,
        hide_if_empty=True,
    )

    def get_children(self, request, parent):
        return parent.get_related_ips().restrict(request.user, 'view')


#
# VLAN groups
#

@register_model_view(VLANGroup, 'list', path='', detail=False)
class VLANGroupListView(generic.ObjectListView):
    queryset = VLANGroup.objects.annotate_utilization()
    filterset = filtersets.VLANGroupFilterSet
    filterset_form = forms.VLANGroupFilterForm
    table = tables.VLANGroupTable


@register_model_view(VLANGroup)
class VLANGroupView(GetRelatedModelsMixin, generic.ObjectView):
    queryset = VLANGroup.objects.annotate_utilization()
    layout = layout.SimpleLayout(
        left_panels=[
            panels.VLANGroupPanel(),
            TagsPanel(),
        ],
        right_panels=[
            RelatedObjectsPanel(),
            CommentsPanel(),
            CustomFieldsPanel(),
        ],
    )

    def get_extra_context(self, request, instance):
        return {
            'related_models': self.get_related_models(request, instance),
        }


@register_model_view(VLANGroup, 'add', detail=False)
@register_model_view(VLANGroup, 'edit')
class VLANGroupEditView(generic.ObjectEditView):
    queryset = VLANGroup.objects.all()
    form = forms.VLANGroupForm


@register_model_view(VLANGroup, 'delete')
class VLANGroupDeleteView(generic.ObjectDeleteView):
    queryset = VLANGroup.objects.all()


@register_model_view(VLANGroup, 'bulk_import', path='import', detail=False)
class VLANGroupBulkImportView(generic.BulkImportView):
    queryset = VLANGroup.objects.all()
    model_form = forms.VLANGroupImportForm


@register_model_view(VLANGroup, 'bulk_edit', path='edit', detail=False)
class VLANGroupBulkEditView(generic.BulkEditView):
    queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
    filterset = filtersets.VLANGroupFilterSet
    table = tables.VLANGroupTable
    form = forms.VLANGroupBulkEditForm


@register_model_view(VLANGroup, 'bulk_rename', path='rename', detail=False)
class VLANGroupBulkRenameView(generic.BulkRenameView):
    queryset = VLANGroup.objects.all()
    filterset = filtersets.VLANGroupFilterSet


@register_model_view(VLANGroup, 'bulk_delete', path='delete', detail=False)
class VLANGroupBulkDeleteView(generic.BulkDeleteView):
    queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
    filterset = filtersets.VLANGroupFilterSet
    table = tables.VLANGroupTable


@register_model_view(VLANGroup, 'vlans')
class VLANGroupVLANsView(ChildAvailabilityMixin, generic.ObjectChildrenView):
    queryset = VLANGroup.objects.all()
    child_model = VLAN
    table = tables.VLANTable
    filterset = filtersets.VLANFilterSet
    filterset_form = forms.VLANFilterForm
    tab = ViewTab(
        label=_('VLANs'),
        badge=lambda x: x.get_child_vlans().count(),
        permission='ipam.view_vlan',
        weight=500
    )

    def get_children(self, request, parent):
        return parent.get_child_vlans().restrict(request.user, 'view').prefetch_related(
            Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user)),
            'tenant', 'site', 'role',
        )

    def prep_table_data(self, request, queryset, parent):
        # Skip synthetic available rows under active filters: filtered-out VLANs would otherwise look available.
        if not get_table_ordering(request, self.table) and not self._has_active_child_filters(request):
            return add_available_vlans(queryset, parent)

        return super().prep_table_data(request, queryset, parent)


#
# VLAN Translation Policies
#

@register_model_view(VLANTranslationPolicy, 'list', path='', detail=False)
class VLANTranslationPolicyListView(generic.ObjectListView):
    queryset = VLANTranslationPolicy.objects.annotate(
        rule_count=count_related(VLANTranslationRule, 'policy'),
    )
    filterset = filtersets.VLANTranslationPolicyFilterSet
    filterset_form = forms.VLANTranslationPolicyFilterForm
    table = tables.VLANTranslationPolicyTable


@register_model_view(VLANTranslationPolicy)
class VLANTranslationPolicyView(generic.ObjectView):
    queryset = VLANTranslationPolicy.objects.all()
    template_name = 'generic/object.html'
    layout = layout.SimpleLayout(
        left_panels=[
            panels.VLANTranslationPolicyPanel(),
        ],
        right_panels=[
            TagsPanel(),
            CustomFieldsPanel(),
            CommentsPanel(),
        ],
        bottom_panels=[
            ObjectsTablePanel(
                'ipam.vlantranslationrule',
                filters={'policy_id': lambda ctx: ctx['object'].pk},
                title=_('VLAN translation rules'),
                exclude_columns=['policy'],
                actions=[
                    actions.AddObject(
                        'ipam.vlantranslationrule',
                        url_params={'policy': lambda ctx: ctx['object'].pk},
                        label=_('Add Rule'),
                    ),
                ],
            ),
        ],
    )


@register_model_view(VLANTranslationPolicy, 'add', detail=False)
@register_model_view(VLANTranslationPolicy, 'edit')
class VLANTranslationPolicyEditView(generic.ObjectEditView):
    queryset = VLANTranslationPolicy.objects.all()
    form = forms.VLANTranslationPolicyForm


@register_model_view(VLANTranslationPolicy, 'delete')
class VLANTranslationPolicyDeleteView(generic.ObjectDeleteView):
    queryset = VLANTranslationPolicy.objects.all()


@register_model_view(VLANTranslationPolicy, 'bulk_import', path='import', detail=False)
class VLANTranslationPolicyBulkImportView(generic.BulkImportView):
    queryset = VLANTranslationPolicy.objects.all()
    model_form = forms.VLANTranslationPolicyImportForm


@register_model_view(VLANTranslationPolicy, 'bulk_edit', path='edit', detail=False)
class VLANTranslationPolicyBulkEditView(generic.BulkEditView):
    queryset = VLANTranslationPolicy.objects.all()
    filterset = filtersets.VLANTranslationPolicyFilterSet
    table = tables.VLANTranslationPolicyTable
    form = forms.VLANTranslationPolicyBulkEditForm


@register_model_view(VLANTranslationPolicy, 'bulk_rename', path='rename', detail=False)
class VLANTranslationPolicyBulkRenameView(generic.BulkRenameView):
    queryset = VLANTranslationPolicy.objects.all()
    filterset = filtersets.VLANTranslationPolicyFilterSet


@register_model_view(VLANTranslationPolicy, 'bulk_delete', path='delete', detail=False)
class VLANTranslationPolicyBulkDeleteView(generic.BulkDeleteView):
    queryset = VLANTranslationPolicy.objects.all()
    filterset = filtersets.VLANTranslationPolicyFilterSet
    table = tables.VLANTranslationPolicyTable


#
# VLAN Translation Rules
#

@register_model_view(VLANTranslationRule, 'list', path='', detail=False)
class VLANTranslationRuleListView(generic.ObjectListView):
    queryset = VLANTranslationRule.objects.all()
    filterset = filtersets.VLANTranslationRuleFilterSet
    filterset_form = forms.VLANTranslationRuleFilterForm
    table = tables.VLANTranslationRuleTable
    actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)


@register_model_view(VLANTranslationRule)
class VLANTranslationRuleView(generic.ObjectView):
    queryset = VLANTranslationRule.objects.all()
    template_name = 'generic/object.html'
    layout = layout.SimpleLayout(
        left_panels=[
            panels.VLANTranslationRulePanel(),
        ],
        right_panels=[
            TagsPanel(),
            CustomFieldsPanel(),
        ],
    )


@register_model_view(VLANTranslationRule, 'add', detail=False)
@register_model_view(VLANTranslationRule, 'edit')
class VLANTranslationRuleEditView(generic.ObjectEditView):
    queryset = VLANTranslationRule.objects.all()
    form = forms.VLANTranslationRuleForm


@register_model_view(VLANTranslationRule, 'delete')
class VLANTranslationRuleDeleteView(generic.ObjectDeleteView):
    queryset = VLANTranslationRule.objects.all()


@register_model_view(VLANTranslationRule, 'bulk_import', path='import', detail=False)
class VLANTranslationRuleBulkImportView(generic.BulkImportView):
    queryset = VLANTranslationRule.objects.all()
    model_form = forms.VLANTranslationRuleImportForm


@register_model_view(VLANTranslationRule, 'bulk_edit', path='edit', detail=False)
class VLANTranslationRuleBulkEditView(generic.BulkEditView):
    queryset = VLANTranslationRule.objects.all()
    filterset = filtersets.VLANTranslationRuleFilterSet
    table = tables.VLANTranslationRuleTable
    form = forms.VLANTranslationRuleBulkEditForm


@register_model_view(VLANTranslationRule, 'bulk_delete', path='delete', detail=False)
class VLANTranslationRuleBulkDeleteView(generic.BulkDeleteView):
    queryset = VLANTranslationRule.objects.all()
    filterset = filtersets.VLANTranslationRuleFilterSet
    table = tables.VLANTranslationRuleTable


#
# FHRP groups
#

@register_model_view(FHRPGroup, 'list', path='', detail=False)
class FHRPGroupListView(generic.ObjectListView):
    queryset = FHRPGroup.objects.annotate(
        member_count=count_related(FHRPGroupAssignment, 'group')
    )
    filterset = filtersets.FHRPGroupFilterSet
    filterset_form = forms.FHRPGroupFilterForm
    table = tables.FHRPGroupTable


@register_model_view(FHRPGroup)
class FHRPGroupView(GetRelatedModelsMixin, generic.ObjectView):
    queryset = FHRPGroup.objects.annotate(
        member_count=count_related(FHRPGroupAssignment, 'group')
    )
    layout = layout.SimpleLayout(
        left_panels=[
            panels.FHRPGroupPanel(),
            TagsPanel(),
            CommentsPanel(),
        ],
        right_panels=[
            panels.FHRPGroupAuthPanel(),
            RelatedObjectsPanel(),
            CustomFieldsPanel(),
        ],
        bottom_panels=[
            ObjectsTablePanel(
                'ipam.ipaddress',
                filters={'fhrpgroup_id': lambda ctx: ctx['object'].pk},
                title=_('Virtual IP addresses'),
                actions=[
                    actions.AddObject(
                        'ipam.ipaddress',
                        url_params={'fhrpgroup': lambda ctx: ctx['object'].pk},
                        label=_('Add IP Address'),
                    ),
                ],
            ),
            ContextTablePanel('members_table', title=_('Members')),
        ],
    )

    def get_extra_context(self, request, instance):
        # Get assigned interfaces
        members_table = tables.FHRPGroupAssignmentTable(
            data=FHRPGroupAssignment.objects.restrict(request.user, 'view').filter(group=instance),
            orderable=False
        )
        members_table.configure(request)
        members_table.columns.hide('group')

        return {
            'related_models': self.get_related_models(
                request, instance,
                extra=(
                    (
                        Service.objects.restrict(request.user, 'view').filter(
                            parent_object_type=ContentType.objects.get_for_model(FHRPGroup),
                            parent_object_id=instance.id,
                        ),
                        'fhrpgroup_id'
                    ),
                ),
            ),
            'members_table': members_table,
        }


@register_model_view(FHRPGroup, 'add', detail=False)
@register_model_view(FHRPGroup, 'edit')
class FHRPGroupEditView(generic.ObjectEditView):
    queryset = FHRPGroup.objects.all()
    form = forms.FHRPGroupForm

    def get_return_url(self, request, obj=None):
        return_url = super().get_return_url(request, obj)

        # If we're redirecting the user to the FHRPGroupAssignment creation form,
        # initialize the group field with the FHRPGroup we just saved.
        if return_url.startswith(reverse('ipam:fhrpgroupassignment_add')):
            return_url += f'&group={obj.pk}'

        return return_url

    def alter_object(self, obj, request, url_args, url_kwargs):
        # Workaround to solve #10719. Capture the current user on the FHRPGroup instance so that
        # we can evaluate permissions during the creation of a new IPAddress within the form.
        obj._user = request.user
        return obj


@register_model_view(FHRPGroup, 'delete')
class FHRPGroupDeleteView(generic.ObjectDeleteView):
    queryset = FHRPGroup.objects.all()


@register_model_view(FHRPGroup, 'bulk_import', path='import', detail=False)
class FHRPGroupBulkImportView(generic.BulkImportView):
    queryset = FHRPGroup.objects.all()
    model_form = forms.FHRPGroupImportForm


@register_model_view(FHRPGroup, 'bulk_edit', path='edit', detail=False)
class FHRPGroupBulkEditView(generic.BulkEditView):
    queryset = FHRPGroup.objects.all()
    filterset = filtersets.FHRPGroupFilterSet
    table = tables.FHRPGroupTable
    form = forms.FHRPGroupBulkEditForm


@register_model_view(FHRPGroup, 'bulk_rename', path='rename', detail=False)
class FHRPGroupBulkRenameView(generic.BulkRenameView):
    queryset = FHRPGroup.objects.all()
    filterset = filtersets.FHRPGroupFilterSet


@register_model_view(FHRPGroup, 'bulk_delete', path='delete', detail=False)
class FHRPGroupBulkDeleteView(generic.BulkDeleteView):
    queryset = FHRPGroup.objects.all()
    filterset = filtersets.FHRPGroupFilterSet
    table = tables.FHRPGroupTable


#
# FHRP group assignments
#

@register_model_view(FHRPGroupAssignment, 'add', detail=False)
@register_model_view(FHRPGroupAssignment, 'edit')
class FHRPGroupAssignmentEditView(generic.ObjectEditView):
    queryset = FHRPGroupAssignment.objects.all()
    form = forms.FHRPGroupAssignmentForm

    def alter_object(self, instance, request, args, kwargs):
        if not instance.pk:
            # Assign the interface based on URL kwargs
            content_type = get_object_or_404(ContentType, pk=request.GET.get('interface_type'))
            instance.interface = get_object_or_404(content_type.model_class(), pk=request.GET.get('interface_id'))
        return instance

    def get_extra_addanother_params(self, request):
        return {
            'interface_type': request.GET.get('interface_type'),
            'interface_id': request.GET.get('interface_id'),
        }


@register_model_view(FHRPGroupAssignment, 'delete')
class FHRPGroupAssignmentDeleteView(generic.ObjectDeleteView):
    queryset = FHRPGroupAssignment.objects.all()


#
# VLANs
#

@register_model_view(VLAN, 'list', path='', detail=False)
class VLANListView(generic.ObjectListView):
    queryset = VLAN.objects.all()
    filterset = filtersets.VLANFilterSet
    filterset_form = forms.VLANFilterForm
    table = tables.VLANTable


@register_model_view(VLAN)
class VLANView(generic.ObjectView):
    queryset = VLAN.objects.all()
    layout = layout.SimpleLayout(
        left_panels=[
            panels.VLANPanel(),
        ],
        right_panels=[
            CustomFieldsPanel(),
            TagsPanel(),
            CommentsPanel(),
        ],
        bottom_panels=[
            ObjectsTablePanel(
                'ipam.prefix',
                filters={'vlan_id': lambda ctx: ctx['object'].pk},
                title=_('Prefixes'),
                exclude_columns=['vlan'],
                actions=[
                    actions.AddObject(
                        'ipam.prefix',
                        url_params={
                            'tenant': lambda ctx: ctx['object'].tenant_id,
                            'scope_type': lambda ctx: (
                                ContentType.objects.get_for_model(Site).pk if ctx['object'].site_id else None
                            ),
                            'scope': lambda ctx: ctx['object'].site_id,
                            'vlan': lambda ctx: ctx['object'].pk,
                        },
                        label=_('Add a Prefix'),
                    ),
                ],
            ),
            panels.VLANCustomerVLANsPanel(),
        ],
    )


@register_model_view(VLAN, 'interfaces')
class VLANInterfacesView(generic.ObjectChildrenView):
    queryset = VLAN.objects.all()
    child_model = Interface
    table = tables.VLANDevicesTable
    filterset = InterfaceFilterSet
    filterset_form = InterfaceFilterForm
    tab = ViewTab(
        label=_('Device Interfaces'),
        badge=lambda x: x.get_interfaces().count(),
        permission='dcim.view_interface',
        weight=500
    )

    def get_children(self, request, parent):
        return parent.get_interfaces().restrict(request.user, 'view')


@register_model_view(VLAN, 'vminterfaces', path='vm-interfaces')
class VLANVMInterfacesView(generic.ObjectChildrenView):
    queryset = VLAN.objects.all()
    child_model = VMInterface
    table = tables.VLANVirtualMachinesTable
    filterset = VMInterfaceFilterSet
    filterset_form = VMInterfaceFilterForm
    tab = ViewTab(
        label=_('VM Interfaces'),
        badge=lambda x: x.get_vminterfaces().count(),
        permission='virtualization.view_vminterface',
        weight=510
    )

    def get_children(self, request, parent):
        return parent.get_vminterfaces().restrict(request.user, 'view')


@register_model_view(VLAN, 'add', detail=False)
@register_model_view(VLAN, 'edit')
class VLANEditView(generic.ObjectEditView):
    queryset = VLAN.objects.all()
    form = forms.VLANForm
    template_name = 'ipam/vlan_edit.html'


@register_model_view(VLAN, 'delete')
class VLANDeleteView(generic.ObjectDeleteView):
    queryset = VLAN.objects.all()


@register_model_view(VLAN, 'bulk_add', path='bulk-add', detail=False)
class VLANBulkCreateView(generic.BulkCreateView):
    queryset = VLAN.objects.all()
    form = forms.VLANIDBulkCreateForm
    model_form = forms.VLANBulkAddForm
    pattern_target = 'vid'
    pattern_template_fields = ('name',)
    template_name = 'ipam/vlan_bulk_add.html'


@register_model_view(VLAN, 'bulk_import', path='import', detail=False)
class VLANBulkImportView(generic.BulkImportView):
    queryset = VLAN.objects.all()
    model_form = forms.VLANImportForm


@register_model_view(VLAN, 'bulk_edit', path='edit', detail=False)
class VLANBulkEditView(generic.BulkEditView):
    queryset = VLAN.objects.all()
    filterset = filtersets.VLANFilterSet
    table = tables.VLANTable
    form = forms.VLANBulkEditForm


@register_model_view(VLAN, 'bulk_rename', path='rename', detail=False)
class VLANBulkRenameView(generic.BulkRenameView):
    queryset = VLAN.objects.all()
    filterset = filtersets.VLANFilterSet


@register_model_view(VLAN, 'bulk_delete', path='delete', detail=False)
class VLANBulkDeleteView(generic.BulkDeleteView):
    queryset = VLAN.objects.all()
    filterset = filtersets.VLANFilterSet
    table = tables.VLANTable


#
# Service templates
#

@register_model_view(ServiceTemplate, 'list', path='', detail=False)
class ServiceTemplateListView(generic.ObjectListView):
    queryset = ServiceTemplate.objects.all()
    filterset = filtersets.ServiceTemplateFilterSet
    filterset_form = forms.ServiceTemplateFilterForm
    table = tables.ServiceTemplateTable


@register_model_view(ServiceTemplate)
class ServiceTemplateView(generic.ObjectView):
    queryset = ServiceTemplate.objects.all()
    template_name = 'generic/object.html'
    layout = layout.SimpleLayout(
        left_panels=[
            panels.ServiceTemplatePanel(),
        ],
        right_panels=[
            CustomFieldsPanel(),
            TagsPanel(),
            CommentsPanel(),
        ],
    )


@register_model_view(ServiceTemplate, 'add', detail=False)
@register_model_view(ServiceTemplate, 'edit')
class ServiceTemplateEditView(generic.ObjectEditView):
    queryset = ServiceTemplate.objects.all()
    form = forms.ServiceTemplateForm


@register_model_view(ServiceTemplate, 'delete')
class ServiceTemplateDeleteView(generic.ObjectDeleteView):
    queryset = ServiceTemplate.objects.all()


@register_model_view(ServiceTemplate, 'bulk_import', path='import', detail=False)
class ServiceTemplateBulkImportView(generic.BulkImportView):
    queryset = ServiceTemplate.objects.all()
    model_form = forms.ServiceTemplateImportForm


@register_model_view(ServiceTemplate, 'bulk_edit', path='edit', detail=False)
class ServiceTemplateBulkEditView(generic.BulkEditView):
    queryset = ServiceTemplate.objects.all()
    filterset = filtersets.ServiceTemplateFilterSet
    table = tables.ServiceTemplateTable
    form = forms.ServiceTemplateBulkEditForm


@register_model_view(ServiceTemplate, 'bulk_rename', path='rename', detail=False)
class ServiceTemplateBulkRenameView(generic.BulkRenameView):
    queryset = ServiceTemplate.objects.all()
    filterset = filtersets.ServiceTemplateFilterSet


@register_model_view(ServiceTemplate, 'bulk_delete', path='delete', detail=False)
class ServiceTemplateBulkDeleteView(generic.BulkDeleteView):
    queryset = ServiceTemplate.objects.all()
    filterset = filtersets.ServiceTemplateFilterSet
    table = tables.ServiceTemplateTable


#
# Services
#

@register_model_view(Service, 'list', path='', detail=False)
class ServiceListView(generic.ObjectListView):
    queryset = Service.objects.prefetch_related('parent')
    filterset = filtersets.ServiceFilterSet
    filterset_form = forms.ServiceFilterForm
    table = tables.ServiceTable


@register_model_view(Service)
class ServiceView(generic.ObjectView):
    queryset = Service.objects.all()
    layout = layout.SimpleLayout(
        left_panels=[
            panels.ServicePanel(),
        ],
        right_panels=[
            CustomFieldsPanel(),
            TagsPanel(),
            CommentsPanel(),
        ],
    )

    def get_extra_context(self, request, instance):
        context = {}
        match instance.parent:
            case Device():
                context['breadcrumb_queryparam'] = 'device_id'
            case VirtualMachine():
                context['breadcrumb_queryparam'] = 'virtual_machine_id'
            case FHRPGroup():
                context['breadcrumb_queryparam'] = 'fhrpgroup_id'

        return context


@register_model_view(Service, 'add', detail=False)
class ServiceCreateView(generic.ObjectEditView):
    queryset = Service.objects.all()
    form = forms.ServiceCreateForm


@register_model_view(Service, 'edit')
class ServiceEditView(generic.ObjectEditView):
    queryset = Service.objects.all()
    form = forms.ServiceForm


@register_model_view(Service, 'delete')
class ServiceDeleteView(generic.ObjectDeleteView):
    queryset = Service.objects.all()


@register_model_view(Service, 'bulk_import', path='import', detail=False)
class ServiceBulkImportView(generic.BulkImportView):
    queryset = Service.objects.all()
    model_form = forms.ServiceImportForm


@register_model_view(Service, 'bulk_edit', path='edit', detail=False)
class ServiceBulkEditView(generic.BulkEditView):
    queryset = Service.objects.prefetch_related('parent')
    filterset = filtersets.ServiceFilterSet
    table = tables.ServiceTable
    form = forms.ServiceBulkEditForm


@register_model_view(Service, 'bulk_rename', path='rename', detail=False)
class ServiceBulkRenameView(generic.BulkRenameView):
    queryset = Service.objects.all()
    filterset = filtersets.ServiceFilterSet


@register_model_view(Service, 'bulk_delete', path='delete', detail=False)
class ServiceBulkDeleteView(generic.BulkDeleteView):
    queryset = Service.objects.prefetch_related('parent')
    filterset = filtersets.ServiceFilterSet
    table = tables.ServiceTable
