import json

from core.choices import ObjectChangeActionChoices
from dcim.models import Device
from dcim.models import Site
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.db import models
from django.db import transaction
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import redirect
from django.shortcuts import render
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext as _
from django.views.generic import View
from django_tables2 import RequestConfig
from ipfabric.diagrams import Network
from ipfabric.diagrams import NetworkSettings
from netbox.object_actions import AddObject
from netbox.object_actions import BulkDelete
from netbox.object_actions import BulkEdit
from netbox.object_actions import BulkExport
from netbox.object_actions import BulkRename
from netbox.views import generic
from netbox.views.generic.base import BaseObjectView
from netbox_branching.models import ChangeDiff
from utilities.data import shallow_compare_dict
from utilities.forms import ConfirmationForm
from utilities.forms import restrict_form_fields
from utilities.paginator import EnhancedPaginator
from utilities.paginator import get_paginate_count
from utilities.query import count_related
from utilities.views import get_viewname
from utilities.views import GetRelatedModelsMixin
from utilities.views import register_model_view
from utilities.views import ViewTab

from .choices import IPFabricSourceTypeChoices
from .filtersets import IPFabricDataFilterSet
from .filtersets import IPFabricEndpointFilterSet
from .filtersets import IPFabricFilterExpressionFilterSet
from .filtersets import IPFabricFilterFilterSet
from .filtersets import IPFabricIngestionChangeFilterSet
from .filtersets import IPFabricIngestionFilterSet
from .filtersets import IPFabricIngestionIssueFilterSet
from .filtersets import IPFabricSnapshotFilterSet
from .filtersets import IPFabricSourceFilterSet
from .filtersets import IPFabricSyncFilterSet
from .filtersets import IPFabricTransformMapFilterSet
from .filtersets import IPFabricTransformMapGroupFilterSet
from .forms import IPFabricFilterBulkEditForm
from .forms import IPFabricFilterBulkImportForm
from .forms import IPFabricFilterExpressionBulkEditForm
from .forms import IPFabricFilterExpressionBulkImportForm
from .forms import IPFabricFilterExpressionForm
from .forms import IPFabricFilterForm
from .forms import IPFabricIngestionFilterForm
from .forms import IPFabricIngestionMergeForm
from .forms import IPFabricRelationshipFieldForm
from .forms import IPFabricSnapshotFilterForm
from .forms import IPFabricSourceBulkEditForm
from .forms import IPFabricSourceFilterForm
from .forms import IPFabricSourceForm
from .forms import IPFabricSyncBulkEditForm
from .forms import IPFabricSyncForm
from .forms import IPFabricTableForm
from .forms import IPFabricTransformFieldForm
from .forms import IPFabricTransformMapBulkEditForm
from .forms import IPFabricTransformMapBulkImportForm
from .forms import IPFabricTransformMapCloneForm
from .forms import IPFabricTransformMapForm
from .forms import IPFabricTransformMapGroupBulkEditForm
from .forms import IPFabricTransformMapGroupBulkImportForm
from .forms import IPFabricTransformMapGroupForm
from .models import IPFabricData
from .models import IPFabricEndpoint
from .models import IPFabricFilter
from .models import IPFabricFilterExpression
from .models import IPFabricIngestion
from .models import IPFabricIngestionIssue
from .models import IPFabricRelationshipField
from .models import IPFabricSnapshot
from .models import IPFabricSource
from .models import IPFabricSync
from .models import IPFabricTransformField
from .models import IPFabricTransformMap
from .models import IPFabricTransformMapGroup
from .tables import DeviceIPFTable
from .tables import IPFabricDataTable
from .tables import IPFabricEndpointTable
from .tables import IPFabricFilterExpressionTable
from .tables import IPFabricFilterTable
from .tables import IPFabricIngestionChangesTable
from .tables import IPFabricIngestionIssuesTable
from .tables import IPFabricIngestionTable
from .tables import IPFabricRelationshipFieldTable
from .tables import IPFabricSnapshotTable
from .tables import IPFabricSourceTable
from .tables import IPFabricSyncTable
from .tables import IPFabricTransformFieldTable
from .tables import IPFabricTransformMapGroupTable
from .tables import IPFabricTransformMapTable
from .utilities.filters import get_filter_expression_test_candidates
from .utilities.ipfutils import IPFabric
from .utilities.transform_map import build_transform_maps
from .utilities.transform_map import get_transform_map


# region - Base Classes for Combined Expressions Views


class CombinedExpressionsBaseView(LoginRequiredMixin, View):
    """Base view for displaying combined filter expressions in an HTMX modal.

    This base class provides common functionality for views that display
    combined/merged filter expressions. Subclasses must implement
    get_context_data() to provide view-specific context.

    The template (inc/combined_expressions.html) supports three context types:
    - "filter": Display merged expressions for a single IPFabricFilter
    - "endpoint": Display combined filters for an endpoint within a sync
    - "endpoint_all": Display combined filters for an endpoint across all syncs
    """

    template_name = "ipfabric_netbox/inc/combined_expressions.html"

    def get_context_data(self, request, **kwargs):
        """Override this method to provide view-specific context.

        Must return a dict with standardized keys:
        - object: The main object being displayed (IPFabricFilter or IPFabricEndpoint, or None)
        - combined_expressions: The expressions (list for filters, dict for endpoints)
        - context_type: One of "filter", "endpoint", or "endpoint_all"
        - is_empty: Boolean indicating if there are no expressions
        - sync: Optional IPFabricSync object (for endpoint context type only)

        Args:
            request: The HTTP request object
            **kwargs: URL parameters passed to the view

        Returns:
            dict: Context dictionary for template rendering
        """
        raise NotImplementedError(
            "Subclasses must implement get_context_data()"
        )  # pragma: no cover

    def add_no_cache_headers(self, response):
        """Add cache control headers to prevent browser caching.

        Args:
            response: HttpResponse object

        Returns:
            HttpResponse: The response with cache headers added
        """
        response["Cache-Control"] = "no-cache, no-store, must-revalidate"
        response["Pragma"] = "no-cache"
        response["Expires"] = "0"
        return response

    def get(self, request, **kwargs):
        """Handle GET requests by rendering the template with context.

        Args:
            request: The HTTP request object
            **kwargs: URL parameters

        Returns:
            HttpResponse: Rendered template with no-cache headers
        """
        context = self.get_context_data(request, **kwargs)
        response = render(request, self.template_name, context)
        return self.add_no_cache_headers(response)


# endregion


# region - Endpoint (Read-Only)
@register_model_view(IPFabricEndpoint, "list", path="", detail=False)
class IPFabricEndpointListView(generic.ObjectListView):
    queryset = IPFabricEndpoint.objects.annotate(
        filters_count=models.Count("filters", distinct=True)
    )
    table = IPFabricEndpointTable
    filterset = IPFabricEndpointFilterSet
    template_name = "ipfabric_netbox/htmx_list.html"


@register_model_view(IPFabricEndpoint)
class IPFabricEndpointView(GetRelatedModelsMixin, generic.ObjectView):
    queryset = IPFabricEndpoint.objects.all()
    actions = ()

    def get_extra_context(self, request, instance):
        return {
            "related_models": self.get_related_models(
                request,
                instance,
                extra=(
                    (
                        IPFabricFilter.objects.restrict(request.user, "view").filter(
                            endpoints=instance
                        ),
                        "endpoints",
                    ),
                ),
            ),
        }


@register_model_view(
    IPFabricEndpoint,
    name="filters",
    path="filters",
    kwargs={"model": IPFabricEndpoint},
)
class IPFabricEndpointFiltersView(CombinedExpressionsBaseView):
    """Display combined filters for an endpoint with optional sync selection."""

    template_name = "ipfabric_netbox/inc/endpoint_filters_with_selector.html"

    def get_context_data(self, request, **kwargs):
        """Provide context for endpoint filters with sync selection.

        Args:
            request: HTTP request (must be HTMX)
            **kwargs: Must contain 'pk' (endpoint primary key)

        Returns:
            dict: Standardized context with endpoint object, available syncs,
                  and combined filters (optionally filtered by sync)
        """
        endpoint_pk = kwargs.get("pk")
        sync_pk = request.GET.get("sync_pk")
        from_sync = request.GET.get("from_sync") == "true"

        # Return empty context if not HTMX or missing required parameters
        if not request.htmx or not endpoint_pk:
            return {
                "object": None,
                "combined_expressions": {},
                "is_empty": True,
                "context_type": "endpoint_all",
            }

        # Fetch endpoint
        endpoint_obj = IPFabricEndpoint.objects.prefetch_related("filters").get(
            pk=endpoint_pk
        )

        # Determine if we're filtering by sync
        sync_obj = None
        if sync_pk:
            try:
                sync_obj = IPFabricSync.objects.get(pk=sync_pk)
            except IPFabricSync.DoesNotExist:
                pass

        # Combine filters (with optional sync filter)
        combined = endpoint_obj.combine_filters(sync=sync_obj)

        # Determine context type based on sync selection
        context_type = "endpoint" if sync_obj else "endpoint_all"

        context = {
            "object": endpoint_obj,
            "combined_expressions": combined,
            "is_empty": len(combined) == 0,
            "context_type": context_type,
            "from_sync": from_sync,
        }

        # Add sync to context if one is selected
        if sync_obj:
            context["sync"] = sync_obj

        # Only add sync selector context if not coming from a sync view
        if not from_sync:
            context["available_syncs"] = IPFabricSync.objects.all().order_by("name")
            context["selected_sync_pk"] = int(sync_pk) if sync_pk else None

        return context

    def get(self, request, **kwargs):
        """Handle GET requests, using appropriate template based on context.

        Args:
            request: The HTTP request object
            **kwargs: URL parameters

        Returns:
            HttpResponse: Rendered template with no-cache headers
        """
        context = self.get_context_data(request, **kwargs)

        # Determine which template to use
        from_sync = request.GET.get("from_sync") == "true"
        sync_pk_param = request.GET.get("sync_pk")

        if from_sync:
            # Coming from sync view - use simple template without selector
            template_name = "ipfabric_netbox/inc/combined_expressions.html"
        elif sync_pk_param is not None and not from_sync:
            # Sync selection change via dropdown - use content-only template
            template_name = "ipfabric_netbox/inc/combined_expressions_content.html"
        else:
            # Initial load from general endpoints - use template with selector
            template_name = self.template_name

        response = render(request, template_name, context)
        return self.add_no_cache_headers(response)


# endregion

# region - Transform Map Relationship Field


@register_model_view(IPFabricRelationshipField, "add", detail=False)
@register_model_view(IPFabricRelationshipField, "edit")
class IPFabricRelationshipFieldEditView(generic.ObjectEditView):
    queryset = IPFabricRelationshipField.objects.all()
    form = IPFabricRelationshipFieldForm
    default_return_url = "plugins:ipfabric_netbox:ipfabricrelationshipfield_list"


@register_model_view(IPFabricRelationshipField, "delete")
class IPFabricRelationshipFieldDeleteView(generic.ObjectDeleteView):
    queryset = IPFabricRelationshipField.objects.all()
    default_return_url = "plugins:ipfabric_netbox:ipfabricrelationshipfield_list"


@register_model_view(
    IPFabricRelationshipField, "bulk_delete", path="delete", detail=False
)
class IPFabricRelationshipFieldBulkDeleteView(generic.BulkDeleteView):
    queryset = IPFabricRelationshipField.objects.all()
    table = IPFabricRelationshipFieldTable


@register_model_view(IPFabricRelationshipField, "list", path="", detail=False)
class IPFabricRelationshipFieldListView(generic.ObjectListView):
    queryset = IPFabricRelationshipField.objects.all()
    table = IPFabricRelationshipFieldTable
    actions = ()


# endregion
# region - Transform Map Group


@register_model_view(IPFabricTransformMapGroup, "list", path="", detail=False)
class IPFabricTransformMapGroupListView(generic.ObjectListView):
    queryset = IPFabricTransformMapGroup.objects.annotate(
        maps_count=models.Count("transform_maps")
    )
    table = IPFabricTransformMapGroupTable
    filterset = IPFabricTransformMapGroupFilterSet


@register_model_view(IPFabricTransformMapGroup, "add", detail=False)
@register_model_view(IPFabricTransformMapGroup, "edit")
class IPFabricTransformMapGroupEditView(generic.ObjectEditView):
    queryset = IPFabricTransformMapGroup.objects.all()
    form = IPFabricTransformMapGroupForm
    default_return_url = "plugins:ipfabric_netbox:ipfabrictransformmapgroup_list"


@register_model_view(IPFabricTransformMapGroup, "delete")
class IPFabricTransformMapGroupDeleteView(generic.ObjectDeleteView):
    queryset = IPFabricTransformMapGroup.objects.all()
    default_return_url = "plugins:ipfabric_netbox:ipfabrictransformmapgroup_list"


@register_model_view(
    IPFabricTransformMapGroup, "bulk_import", path="import", detail=False
)
class IPFabricTransformMapGroupBulkImportView(generic.BulkImportView):
    queryset = IPFabricTransformMapGroup.objects.all()
    model_form = IPFabricTransformMapGroupBulkImportForm


@register_model_view(IPFabricTransformMapGroup, "bulk_edit", path="edit", detail=False)
class IPFabricTransformMapGroupBulkEditView(generic.BulkEditView):
    queryset = IPFabricTransformMapGroup.objects.all()
    table = IPFabricTransformMapGroupTable
    form = IPFabricTransformMapGroupBulkEditForm


@register_model_view(
    IPFabricTransformMapGroup, "bulk_rename", path="rename", detail=False
)
class IPFabricTransformMapGroupBulkRenameView(generic.BulkRenameView):
    queryset = IPFabricTransformMapGroup.objects.all()


@register_model_view(
    IPFabricTransformMapGroup, "bulk_delete", path="delete", detail=False
)
class IPFabricTransformMapGroupBulkDeleteView(generic.BulkDeleteView):
    queryset = IPFabricTransformMapGroup.objects.all()
    table = IPFabricTransformMapGroupTable


@register_model_view(IPFabricTransformMapGroup)
class IPFabricTransformMapGroupView(GetRelatedModelsMixin, generic.ObjectView):
    queryset = IPFabricTransformMapGroup.objects.all()

    def get_extra_context(self, request, instance):
        return {
            "related_models": self.get_related_models(request, instance, omit=[]),
        }


# endregion
# region - Transform Map


@register_model_view(IPFabricTransformMap, "list", path="", detail=False)
class IPFabricTransformMapListView(generic.ObjectListView):
    queryset = IPFabricTransformMap.objects.all()
    table = IPFabricTransformMapTable
    template_name = "ipfabric_netbox/ipfabrictransformmap_list.html"
    filterset = IPFabricTransformMapFilterSet


@register_model_view(IPFabricTransformMap, "add", detail=False)
@register_model_view(IPFabricTransformMap, "edit")
class IPFabricTransformMapEditView(generic.ObjectEditView):
    queryset = IPFabricTransformMap.objects.all()
    form = IPFabricTransformMapForm
    default_return_url = "plugins:ipfabric_netbox:ipfabrictransformmap_list"


@register_model_view(IPFabricTransformMap, "delete")
class IPFabricTransformMapDeleteView(generic.ObjectDeleteView):
    queryset = IPFabricTransformMap.objects.all()
    default_return_url = "plugins:ipfabric_netbox:ipfabrictransformmap_list"


@register_model_view(IPFabricTransformMap, "bulk_import", path="import", detail=False)
class IPFabricTransformMapBulkImportView(generic.BulkImportView):
    queryset = IPFabricTransformMap.objects.all()
    model_form = IPFabricTransformMapBulkImportForm


@register_model_view(IPFabricTransformMap, "bulk_edit", path="edit", detail=False)
class IPFabricTransformMapBulkEditView(generic.BulkEditView):
    queryset = IPFabricTransformMap.objects.all()
    table = IPFabricTransformMapTable
    form = IPFabricTransformMapBulkEditForm


@register_model_view(IPFabricTransformMap, "bulk_rename", path="rename", detail=False)
class IPFabricTransformMapBulkRenameView(generic.BulkRenameView):
    queryset = IPFabricTransformMap.objects.all()


@register_model_view(IPFabricTransformMap, "bulk_delete", path="delete", detail=False)
class IPFabricTransformMapBulkDeleteView(generic.BulkDeleteView):
    queryset = IPFabricTransformMap.objects.all()
    table = IPFabricTransformMapTable


@register_model_view(IPFabricTransformMap)
class IPFabricTransformMapView(GetRelatedModelsMixin, generic.ObjectView):
    queryset = IPFabricTransformMap.objects.all()

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


@register_model_view(IPFabricTransformMap, "restore", detail=False)
class IPFabricTransformMapRestoreView(generic.ObjectListView):
    queryset = IPFabricTransformMap.objects.all()
    table = IPFabricTransformMapTable

    def get_required_permission(self):
        return "ipfabric_netbox.restore_ipfabrictransformmap"

    def get(self, request):
        if request.htmx:
            viewname = get_viewname(self.queryset.model, action="restore")
            form_url = reverse(viewname)
            form = ConfirmationForm(initial=request.GET)
            dependent_objects = {
                IPFabricTransformMap: IPFabricTransformMap.objects.filter(
                    group__isnull=True
                ),
                IPFabricTransformField: IPFabricTransformField.objects.filter(
                    transform_map__group__isnull=True
                ),
                IPFabricRelationshipField: IPFabricRelationshipField.objects.filter(
                    transform_map__group__isnull=True
                ),
            }
            return render(
                request,
                "ipfabric_netbox/ipfabrictransformmap_restore.html",
                {
                    "form": form,
                    "form_url": form_url,
                    "dependent_objects": dependent_objects,
                },
            )
        return redirect(reverse("plugins:ipfabric_netbox:ipfabrictransformmap_list"))

    def post(self, request):
        IPFabricTransformMap.objects.filter(group__isnull=True).delete()
        build_transform_maps(data=get_transform_map())
        return redirect("plugins:ipfabric_netbox:ipfabrictransformmap_list")


@register_model_view(IPFabricTransformMap, "clone")
class IPFabricTransformMapCloneView(BaseObjectView):
    queryset = IPFabricTransformMap.objects.all()
    template_name = "ipfabric_netbox/inc/clone_form.html"
    form = IPFabricTransformMapCloneForm

    def get_required_permission(self):
        return "ipfabric_netbox.clone_ipfabrictransformmap"

    def get(self, request, pk):
        obj = get_object_or_404(self.queryset, pk=pk)
        if request.htmx:
            viewname = get_viewname(self.queryset.model, action="clone")
            form_url = reverse(viewname, kwargs={"pk": obj.pk})
            initial = request.GET.copy()
            initial["name"] = f"Clone of {obj.name}"
            form = self.form(initial=initial)
            restrict_form_fields(form, request.user)
            return render(
                request,
                self.template_name,
                {
                    "object": obj,
                    "form": form,
                    "pk": pk,
                    "form_url": form_url,
                },
            )
        return redirect(obj.get_absolute_url())

    def post(self, request, pk):
        obj = get_object_or_404(self.queryset, pk=pk)
        form = self.form(request.POST)
        restrict_form_fields(form, request.user)
        try:
            if form.is_valid():
                with transaction.atomic():
                    fields = IPFabricTransformField.objects.filter(transform_map=obj)
                    relationships = IPFabricRelationshipField.objects.filter(
                        transform_map=obj
                    )
                    # Clone the transform map - create a proper copy using Django model copying
                    new_map = IPFabricTransformMap(
                        name=form.cleaned_data["name"],
                        source_endpoint=obj.source_endpoint,
                        target_model=obj.target_model,
                        group=form.cleaned_data["group"],
                    )
                    new_map.full_clean()
                    new_map.save()

                    # Clone related transform fields
                    if form.cleaned_data["clone_fields"]:
                        for field in fields:
                            IPFabricTransformField.objects.create(
                                transform_map=new_map,
                                source_field=field.source_field,
                                target_field=field.target_field,
                                coalesce=field.coalesce,
                                template=field.template,
                            )

                    # Clone related relationship fields
                    if form.cleaned_data["clone_relationships"]:
                        for rel in relationships:
                            IPFabricRelationshipField.objects.create(
                                transform_map=new_map,
                                source_model=rel.source_model,
                                target_field=rel.target_field,
                                coalesce=rel.coalesce,
                                template=rel.template,
                            )

                return_url = reverse(
                    "plugins:ipfabric_netbox:ipfabrictransformmap", args=[new_map.pk]
                )
                if request.htmx:
                    response = HttpResponse()
                    response["HX-Redirect"] = return_url
                    return response
                return redirect(return_url)
        except ValidationError as err:
            if not hasattr(err, "error_dict") or not err.error_dict:
                form.add_error(None, err)
            else:
                # This serves to show errors in the form directly
                for field, error in err.error_dict.items():
                    if field in form.fields:
                        form.add_error(field, error)
                    else:
                        form.add_error(None, error)
        if request.htmx:
            viewname = get_viewname(self.queryset.model, action="clone")
            form_url = reverse(viewname, kwargs={"pk": obj.pk})
            response = render(
                request,
                "ipfabric_netbox/inc/clone_form.html",
                {
                    "form": form,
                    "object": obj,
                    "pk": pk,
                    "form_url": form_url,
                },
            )
            response["X-Debug-HTMX-Partial"] = "true"
            return response
        return render(
            request,
            self.template_name,
            {
                "form": form,
                "object": obj,
                "pk": pk,
            },
        )


@register_model_view(IPFabricTransformMap, "fields")
class IPFabricTransformFieldView(generic.ObjectChildrenView):
    queryset = IPFabricTransformMap.objects.all()
    child_model = IPFabricTransformField
    table = IPFabricTransformFieldTable
    template_name = "ipfabric_netbox/inc/transform_map_field_map.html"
    actions = (AddObject, BulkDelete)
    tab = ViewTab(
        label=_("Field Maps"),
        badge=lambda obj: IPFabricTransformField.objects.filter(
            transform_map=obj
        ).count(),
        permission="ipfabric_netbox.view_ipfabrictransformfield",
    )

    def get_children(self, request, parent):
        return self.child_model.objects.filter(transform_map=parent)


@register_model_view(IPFabricTransformMap, "relationships")
class IPFabricTransformRelationshipView(generic.ObjectChildrenView):
    queryset = IPFabricTransformMap.objects.all()
    child_model = IPFabricRelationshipField
    table = IPFabricRelationshipFieldTable
    template_name = "ipfabric_netbox/inc/transform_map_relationship_map.html"
    actions = (AddObject, BulkDelete)
    tab = ViewTab(
        label=_("Relationship Maps"),
        badge=lambda obj: IPFabricRelationshipField.objects.filter(
            transform_map=obj
        ).count(),
        permission="ipfabric_netbox.view_ipfabricrelationshipfield",
    )

    def get_children(self, request, parent):
        return self.child_model.objects.filter(transform_map=parent)


# endregion
# region - Transform Map Field


@register_model_view(IPFabricTransformField, "list", path="", detail=False)
class IPFabricTransformFieldListView(generic.ObjectListView):
    queryset = IPFabricTransformField.objects.all()
    table = IPFabricTransformFieldTable
    actions = ()


@register_model_view(IPFabricTransformField, "add", detail=False)
@register_model_view(IPFabricTransformField, "edit")
class IPFabricTransformFieldEditView(generic.ObjectEditView):
    queryset = IPFabricTransformField.objects.all()
    form = IPFabricTransformFieldForm


@register_model_view(IPFabricTransformField, "delete")
class IPFabricTransformFieldDeleteView(generic.ObjectDeleteView):
    queryset = IPFabricTransformField.objects.all()


@register_model_view(IPFabricTransformField, "bulk_delete", path="delete", detail=False)
class IPFabricTransformFieldBulkDeleteView(generic.BulkDeleteView):
    queryset = IPFabricTransformField.objects.all()
    table = IPFabricTransformFieldTable


# endregion
# region - Snapshot


@register_model_view(IPFabricSnapshot, "list", path="", detail=False)
class IPFabricSnapshotListView(generic.ObjectListView):
    queryset = IPFabricSnapshot.objects.all()
    table = IPFabricSnapshotTable
    filterset = IPFabricSnapshotFilterSet
    filterset_form = IPFabricSnapshotFilterForm
    actions = (BulkExport, BulkDelete)


@register_model_view(IPFabricSnapshot)
class IPFabricSnapshotView(generic.ObjectView):
    queryset = IPFabricSnapshot.objects.all()


@register_model_view(IPFabricSnapshot, "delete")
class IPFabricSnapshotDeleteView(generic.ObjectDeleteView):
    queryset = IPFabricSnapshot.objects.all()


@register_model_view(IPFabricSnapshot, "bulk_delete", path="delete", detail=False)
class IPFabricSnapshotBulkDeleteView(generic.BulkDeleteView):
    queryset = IPFabricSnapshot.objects.all()
    filterset = IPFabricSnapshotFilterSet
    table = IPFabricSnapshotTable


@register_model_view(IPFabricSnapshot, "data")
class IPFabricSnapshotRawView(generic.ObjectChildrenView):
    queryset = IPFabricSnapshot.objects.all()
    child_model = IPFabricData
    table = IPFabricDataTable
    template_name = "ipfabric_netbox/inc/snapshotdata.html"
    tab = ViewTab(
        label=_("Raw Data"),
        badge=lambda obj: IPFabricData.objects.filter(snapshot_data=obj).count(),
        permission="ipfabric_netbox.view_ipfabricsnapshot",
        hide_if_empty=True,
    )

    def get_children(self, request, parent):
        return self.child_model.objects.filter(snapshot_data=parent)


# endregion
# region - Snapshot Data


@register_model_view(IPFabricData, "delete")
class IPFabricSnapshotDataDeleteView(generic.ObjectDeleteView):
    queryset = IPFabricData.objects.all()


@register_model_view(IPFabricData, "bulk_delete", path="delete", detail=False)
class IPFabricSnapshotDataBulkDeleteView(generic.BulkDeleteView):
    queryset = IPFabricData.objects.all()
    filterset = IPFabricDataFilterSet
    table = IPFabricDataTable


@register_model_view(
    IPFabricData,
    name="data",
    path="json",
    kwargs={},
)
class IPFabricSnapshotDataJSONView(generic.ObjectView):
    queryset = IPFabricData.objects.all()
    template_name = "ipfabric_netbox/inc/json.html"

    def get(self, request, **kwargs):
        data = get_object_or_404(IPFabricData, pk=kwargs.get("pk"))
        if request.htmx:
            return render(
                request,
                self.template_name,
                {
                    "object": data,
                },
            )
        return render(
            request,
            self.template_name,
            {
                "object": data,
            },
        )


# endregion
# region - Source


@register_model_view(IPFabricSource, "list", path="", detail=False)
class IPFabricSourceListView(generic.ObjectListView):
    queryset = IPFabricSource.objects.annotate(
        snapshot_count=count_related(IPFabricSnapshot, "source")
    )
    filterset = IPFabricSourceFilterSet
    filterset_form = IPFabricSourceFilterForm
    table = IPFabricSourceTable
    actions = (AddObject, BulkExport, BulkEdit, BulkRename, BulkDelete)


@register_model_view(IPFabricSource, "add", detail=False)
@register_model_view(IPFabricSource, "edit")
class IPFabricSourceEditView(generic.ObjectEditView):
    queryset = IPFabricSource.objects.all()
    form = IPFabricSourceForm


@register_model_view(IPFabricSource)
class IPFabricSourceView(GetRelatedModelsMixin, generic.ObjectView):
    queryset = IPFabricSource.objects.all()

    def get_extra_context(self, request, instance):
        job = instance.jobs.order_by("id").last()
        data = {
            "related_models": self.get_related_models(request, instance),
            "job": job,
        }
        if job:
            data["job_results"] = job.data
        return data


@register_model_view(IPFabricSource, "sync")
class IPFabricSourceSyncView(BaseObjectView):
    queryset = IPFabricSource.objects.all()

    def get_required_permission(self):
        return "ipfabric_netbox.sync_ipfabricsource"

    def get(self, request, pk):
        ipfabricsource = get_object_or_404(self.queryset, pk=pk)
        return redirect(ipfabricsource.get_absolute_url())

    def post(self, request, pk):
        ipfabricsource = get_object_or_404(self.queryset, pk=pk)
        job = ipfabricsource.enqueue_sync_job(request=request)

        messages.success(request, f"Queued job #{job.pk} to sync {ipfabricsource}")
        return redirect(ipfabricsource.get_absolute_url())


@register_model_view(IPFabricSource, "delete")
class IPFabricSourceDeleteView(generic.ObjectDeleteView):
    queryset = IPFabricSource.objects.all()


@register_model_view(IPFabricSource, "bulk_edit", path="edit", detail=False)
class IPFabricSourceBulkEditView(generic.BulkEditView):
    queryset = IPFabricSource.objects.all()
    table = IPFabricSourceTable
    form = IPFabricSourceBulkEditForm


@register_model_view(IPFabricSource, "bulk_rename", path="rename", detail=False)
class IPFabricSourceBulkRenameView(generic.BulkRenameView):
    queryset = IPFabricSource.objects.all()


@register_model_view(IPFabricSource, "bulk_delete", path="delete", detail=False)
class IPFabricSourceBulkDeleteView(generic.BulkDeleteView):
    queryset = IPFabricSource.objects.all()
    filterset = IPFabricSourceFilterSet
    table = IPFabricSourceTable


@register_model_view(
    IPFabricSource,
    name="topology",
    path="topology/<int:site>",
    kwargs={"snapshot": ""},
)
class IPFabricSourceTopology(LoginRequiredMixin, View):
    template_name = "ipfabric_netbox/inc/site_topology_modal.html"

    def get(self, request, pk, site, **kwargs):
        if request.htmx:
            try:
                site = get_object_or_404(Site, pk=site)
                source_id = request.GET.get("source")
                if not source_id:
                    raise Exception("Source ID not available in request.")
                source = get_object_or_404(IPFabricSource, pk=source_id)
                snapshot = request.GET.get("snapshot")
                if not snapshot:
                    raise Exception("Snapshot ID not available in request.")

                source.parameters.update(
                    {"snapshot_id": snapshot, "base_url": source.url}
                )

                ipf = IPFabric(parameters=source.parameters)
                snapshot_data = ipf.ipf.snapshots.get(snapshot)
                if not snapshot_data:
                    raise Exception(
                        f"Snapshot ({snapshot}) not available in IP Fabric."  # noqa E713
                    )

                sites = ipf.ipf.inventory.sites.all(
                    filters={"siteName": ["eq", site.name]}
                )
                if not sites:
                    raise Exception(
                        f"{site.name} not available in snapshot ({snapshot})."  # noqa E713
                    )

                net = Network(sites=site.name, all_network=False)
                settings = NetworkSettings()
                settings.hide_protocol("xdp")
                settings.hiddenDeviceTypes.extend(["transit", "cloud"])

                link = ipf.ipf.diagram.share_link(net, graph_settings=settings)
                svg_data = ipf.ipf.diagram.svg(net, graph_settings=settings).decode(
                    "utf-8"
                )
                error = None
            except Exception as e:
                error = e
                svg_data = link = snapshot_data = source = None

            return render(
                request,
                self.template_name,
                {
                    "site": site,
                    "source": source,
                    "svg": svg_data,
                    "size": "xl",
                    "link": link,
                    "time": timezone.now(),
                    "snapshot": snapshot_data,
                    "error": error,
                },
            )
        return render(
            request,
            self.template_name,
            {
                "site": site,
                "size": "xl",
                "time": timezone.now(),
            },
        )


# endregion
# region - Sync
@register_model_view(IPFabricSync, "list", path="", detail=False)
class IPFabricSyncListView(generic.ObjectListView):
    queryset = IPFabricSync.objects.annotate(
        last_ingestion_pk=models.Subquery(
            IPFabricIngestion.objects.filter(sync=models.OuterRef("pk"))
            .order_by("-pk")
            .values("pk")[:1]
        )
    )
    table = IPFabricSyncTable
    filterset = IPFabricSyncFilterSet
    actions = (AddObject, BulkExport, BulkEdit, BulkRename, BulkDelete)


@register_model_view(IPFabricSync, "add", detail=False)
@register_model_view(IPFabricSync, "edit")
class IPFabricSyncEditView(generic.ObjectEditView):
    queryset = IPFabricSync.objects.all()
    form = IPFabricSyncForm

    def alter_object(self, obj, request, url_args, url_kwargs):
        obj.user = request.user
        return obj


@register_model_view(IPFabricSync)
class IPFabricSyncView(generic.ObjectView):
    queryset = IPFabricSync.objects.all()

    def get(self, request, **kwargs):
        # Handle HTMX requests separately
        if request.htmx:
            instance = self.get_object(**kwargs)
            last_ingestion = instance.ipfabricingestion_set.last()

            response = render(
                request,
                "ipfabric_netbox/partials/sync_last_ingestion.html",
                {"last_ingestion": last_ingestion},
            )

            if instance.status not in ["queued", "syncing"]:
                messages.success(
                    request,
                    f"Ingestion ({instance.name}) {instance.status}. Ingestion {last_ingestion.name} {last_ingestion.job.status}.",
                )
                response["HX-Refresh"] = "true"
            return response

        # For regular requests, use the parent method which includes actions
        return super().get(request, **kwargs)

    def get_extra_context(self, request, instance):
        if request.GET.get("format") in ["json", "yaml"]:
            format = request.GET.get("format")
            if request.user.is_authenticated:
                request.user.config.set("data_format", format, commit=True)
        elif request.user.is_authenticated:
            format = request.user.config.get("data_format", "json")
        else:
            format = "json"

        last_ingestion = instance.ipfabricingestion_set.last()

        scheduled_job = None
        if instance.scheduled:
            scheduled_job = instance.jobs.filter(scheduled=instance.scheduled).last()

        return {
            "format": format,
            "last_ingestion": last_ingestion,
            "scheduled_job": scheduled_job,
        }


@register_model_view(IPFabricSync, "sync")
class IPFabricStartSyncView(BaseObjectView):
    queryset = IPFabricSync.objects.all()

    def get_required_permission(self):
        return "ipfabric_netbox.sync_ipfabricsync"

    def get(self, request, pk):
        ipfabric = get_object_or_404(self.queryset, pk=pk)
        return redirect(ipfabric.get_absolute_url())

    def post(self, request, pk):
        ipfabric = get_object_or_404(self.queryset, pk=pk)
        job = ipfabric.enqueue_sync_job(user=request.user, adhoc=True)

        messages.success(request, f"Queued job #{job.pk} to sync {ipfabric}")
        return redirect(ipfabric.get_absolute_url())


@register_model_view(IPFabricSync, "delete")
class IPFabricSyncDeleteView(generic.ObjectDeleteView):
    queryset = IPFabricSync.objects.all()
    default_return_url = "plugins:ipfabric_netbox:ipfabricsync_list"


@register_model_view(IPFabricSync, "bulk_edit", path="edit", detail=False)
class IPFabricSyncBulkEditView(generic.BulkEditView):
    queryset = IPFabricSync.objects.all()
    table = IPFabricSyncTable
    form = IPFabricSyncBulkEditForm


@register_model_view(IPFabricSync, "bulk_rename", path="rename", detail=False)
class IPFabricSyncBulkRenameView(generic.BulkRenameView):
    queryset = IPFabricSync.objects.all()


@register_model_view(IPFabricSync, "bulk_delete", path="delete", detail=False)
class IPFabricSyncBulkDeleteView(generic.BulkDeleteView):
    queryset = IPFabricSync.objects.all()
    filterset = IPFabricSnapshotFilterSet
    table = IPFabricSyncTable


@register_model_view(IPFabricSync, "transformmaps")
class IPFabricTransformMapSyncTabView(generic.ObjectChildrenView):
    queryset = IPFabricSync.objects.all()
    child_model = IPFabricTransformMap
    table = IPFabricTransformMapTable
    template_name = "generic/object_children.html"
    actions = (AddObject, BulkDelete)
    tab = ViewTab(
        label=_("Transform Maps"),
        badge=lambda obj: obj.get_transform_maps(
            obj.parameters.get("groups", []) if obj.parameters else []
        ).count(),
        permission="ipfabric_netbox.view_ipfabrictransformmap",
    )

    def get_children(self, request, parent):
        return parent.get_transform_maps(
            parent.parameters.get("groups", []) if parent.parameters else []
        )


@register_model_view(IPFabricSync, "filters")
class IPFabricTransformMapFilterTabView(generic.ObjectChildrenView):
    queryset = IPFabricSync.objects.all()
    child_model = IPFabricFilter
    table = IPFabricFilterTable
    template_name = "generic/object_children.html"
    actions = (AddObject, BulkDelete)
    tab = ViewTab(
        label=_("Filters"),
        badge=lambda obj: obj.filters.count(),
        permission="ipfabric_netbox.view_ipfabricfilter",
    )

    def get_children(self, request, parent):
        return parent.filters.all()


@register_model_view(IPFabricSync, "endpoints")
class IPFabricSyncEndpointTabView(generic.ObjectChildrenView):
    queryset = IPFabricSync.objects.all()
    child_model = IPFabricEndpoint
    table = IPFabricEndpointTable
    template_name = "generic/object_children.html"
    actions = ()
    tab = ViewTab(
        label=_("Endpoints"),
        badge=lambda obj: IPFabricEndpoint.objects.filter(filters__syncs=obj)
        .distinct()
        .count(),
        permission="ipfabric_netbox.view_ipfabricendpoint",
    )

    def get_children(self, request, parent):
        # Get all unique endpoints used by filters in this sync
        return (
            IPFabricEndpoint.objects.filter(filters__syncs=parent)
            .distinct()
            .annotate(
                sync_pk=models.Value(parent.pk, output_field=models.IntegerField()),
                filters_count=models.Count(
                    "filters", filter=models.Q(filters__syncs=parent)
                ),
            )
        )

    def get_table(self, *args, **kwargs):
        table = super().get_table(*args, **kwargs)
        # Override default columns to show the context-specific ones
        table.default_columns = ("name", "endpoint", "filters_count", "show_filters")
        return table


@register_model_view(
    IPFabricSync,
    name="endpoint_filters",
    path="endpoint/<int:endpoint_pk>/filters",
    kwargs={"model": IPFabricSync},
)
class IPFabricSyncEndpointFiltersView(CombinedExpressionsBaseView):
    """Display combined filters for an endpoint within a specific sync."""

    def get_context_data(self, request, **kwargs):
        """Provide context for endpoint filters within a sync.

        Args:
            request: HTTP request (must be HTMX)
            **kwargs: Must contain 'pk' (sync primary key) and 'endpoint_pk'

        Returns:
            dict: Standardized context with endpoint, sync, and combined filters
        """
        sync_pk = kwargs.get("pk")
        endpoint_pk = kwargs.get("endpoint_pk")

        # Return empty context if not HTMX or missing required parameters
        if not request.htmx or not sync_pk or not endpoint_pk:
            return {
                "object": None,
                "combined_expressions": {},
                "is_empty": True,
                "context_type": "endpoint",
            }

        # Fetch sync and endpoint objects
        sync_obj = IPFabricSync.objects.get(pk=sync_pk)
        endpoint_obj = IPFabricEndpoint.objects.get(pk=endpoint_pk)

        # Combine filters for this endpoint within this sync
        combined = endpoint_obj.combine_filters(sync=sync_obj)

        return {
            "object": endpoint_obj,  # The endpoint is the main object
            "sync": sync_obj,  # Pass sync for template context
            "combined_expressions": combined,
            "is_empty": len(combined) == 0,
            "context_type": "endpoint",
        }


@register_model_view(IPFabricSync, "ingestion")
class IPFabricIngestionTabView(generic.ObjectChildrenView):
    queryset = IPFabricSync.objects.all()
    child_model = IPFabricIngestion
    table = IPFabricIngestionTable
    filterset = IPFabricIngestionFilterSet
    actions = (AddObject, BulkDelete)
    tab = ViewTab(
        label=_("Ingestions"),
        badge=lambda obj: IPFabricIngestion.objects.filter(sync=obj).count(),
        permission="ipfabric_netbox.view_ipfabricingestion",
    )

    def get_children(self, request, parent):
        return self.child_model.objects.filter(sync=parent).annotate(
            description=models.F("branch__description"),
            user=models.F("sync__user__username"),
            staged_changes=models.Count(models.F("branch__changediff")),
            branch_name=models.F("branch__name"),
            sync_name=models.F("sync__name"),
        )


# endregion
# region - Ingestion


@register_model_view(IPFabricIngestion, "list", path="", detail=False)
class IPFabricIngestionListView(generic.ObjectListView):
    queryset = IPFabricIngestion.objects.annotate(
        description=models.F("branch__description"),
        user=models.F("sync__user__username"),
        staged_changes=models.Count(models.F("branch__changediff")),
        branch_name=models.F("branch__name"),
        sync_name=models.F("sync__name"),
    )
    filterset = IPFabricIngestionFilterSet
    filterset_form = IPFabricIngestionFilterForm
    table = IPFabricIngestionTable
    actions = (AddObject, BulkExport, BulkDelete)


def annotate_statistics(queryset):
    return queryset.annotate(
        num_created=models.Count(
            "branch__changediff",
            filter=models.Q(
                branch__changediff__action=ObjectChangeActionChoices.ACTION_CREATE
            )
            & ~models.Q(branch__changediff__object_type__model="objectchange"),
        ),
        num_updated=models.Count(
            "branch__changediff",
            filter=models.Q(
                branch__changediff__action=ObjectChangeActionChoices.ACTION_UPDATE
            )
            & ~models.Q(branch__changediff__object_type__model="objectchange"),
        ),
        num_deleted=models.Count(
            "branch__changediff",
            filter=models.Q(
                branch__changediff__action=ObjectChangeActionChoices.ACTION_DELETE
            )
            & ~models.Q(branch__changediff__object_type__model="objectchange"),
        ),
        description=models.F("branch__description"),
        user=models.F("sync__user__username"),
        staged_changes=models.Count(models.F("branch__changediff")),
    )


@register_model_view(
    IPFabricIngestion,
    name="logs",
    path="logs",
)
class IPFabricIngestionLogView(LoginRequiredMixin, View):
    template_name = "ipfabric_netbox/partials/ingestion_all.html"

    def get(self, request, **kwargs):
        ingestion_id = kwargs.get("pk")
        ingestion = annotate_statistics(IPFabricIngestion.objects).get(pk=ingestion_id)
        data = ingestion.get_statistics()
        data["object"] = ingestion
        data["job"] = ingestion.job

        if request.htmx:
            response = render(
                request,
                self.template_name,
                data,
            )
            if not ingestion.job or ingestion.job.completed:
                response["HX-Refresh"] = "true"
            return response
        return render(request, self.template_name, data)


@register_model_view(IPFabricIngestion)
class IPFabricIngestionView(generic.ObjectView):
    queryset = annotate_statistics(IPFabricIngestion.objects)

    def get_extra_context(self, request, instance):
        data = instance.get_statistics()
        return data


@register_model_view(IPFabricIngestion, "merge")
class IPFabricIngestionMergeView(BaseObjectView):
    queryset = IPFabricIngestion.objects.annotate(
        description=models.F("branch__description"),
        user=models.F("sync__user__username"),
        staged_changes=models.Count(models.F("branch__changediff")),
    )
    template_name = "ipfabric_netbox/inc/merge_form.html"
    form = IPFabricIngestionMergeForm

    def get_required_permission(self):
        return "ipfabric_netbox.merge_ipfabricingestion"

    def get(self, request, pk):
        obj = get_object_or_404(self.queryset, pk=pk)

        if request.htmx:
            viewname = get_viewname(self.queryset.model, action="merge")
            form_url = reverse(viewname, kwargs={"pk": obj.pk})
            form = self.form(initial=request.GET)
            restrict_form_fields(form, request.user)
            return render(
                request,
                "ipfabric_netbox/inc/merge_form.html",
                {
                    "object": obj,
                    "object_type": self.queryset.model._meta.verbose_name,
                    "form": form,
                    "form_url": form_url,
                    **self.get_extra_context(request, obj),
                },
            )

        return redirect(obj.get_absolute_url())

    def post(self, request, pk):
        ingestion = get_object_or_404(self.queryset, pk=pk)
        form = self.form(request.POST)
        restrict_form_fields(form, request.user)
        if form.is_valid():
            job = ingestion.enqueue_merge_job(
                user=request.user, remove_branch=form.cleaned_data["remove_branch"]
            )
            messages.success(request, f"Queued job #{job.pk} to sync {ingestion}")
            return redirect(ingestion.get_absolute_url())

        # Handle invalid form - add form errors to messages and redirect back
        for field, errors in form.errors.items():
            for error in errors:
                messages.error(request, f"{field}: {error}")
        if form.non_field_errors():
            for error in form.non_field_errors():
                messages.error(request, error)

        return redirect(ingestion.get_absolute_url())


@register_model_view(
    IPFabricIngestion,
    name="change_diff",
    path="change/<int:change_pk>",
    kwargs={"model": IPFabricIngestion},
)
class IPFabricIngestionChangesDiffView(LoginRequiredMixin, View):
    template_name = "ipfabric_netbox/inc/diff.html"

    def get(self, request, **kwargs):
        change_id = kwargs.get("change_pk", None)

        if not request.htmx or not change_id:
            return render(
                request,
                self.template_name,
                {
                    "change": None,
                    "prechange_data": None,
                    "postchange_data": None,
                    "diff_added": None,
                    "diff_removed": None,
                    "size": "lg",
                },
            )

        change = ChangeDiff.objects.get(pk=change_id)
        if change.original and change.modified:
            diff_added = shallow_compare_dict(
                change.original or dict(),
                change.modified or dict(),
                exclude=["last_updated"],
            )
            diff_removed = (
                {x: change.original.get(x) for x in diff_added}
                if change.modified
                else {}
            )
        else:
            diff_added = None
            diff_removed = None

        return render(
            request,
            self.template_name,
            {
                "change": change,
                "prechange_data": change.original,
                "postchange_data": change.modified,
                "diff_added": diff_added,
                "diff_removed": diff_removed,
                "size": "lg",
            },
        )


@register_model_view(IPFabricIngestion, "change")
class IPFabricIngestionChangesView(generic.ObjectChildrenView):
    queryset = IPFabricIngestion.objects.all()
    child_model = ChangeDiff
    table = IPFabricIngestionChangesTable
    filterset = IPFabricIngestionChangeFilterSet
    template_name = "generic/object_children.html"
    tab = ViewTab(
        label=_("Changes"),
        badge=lambda obj: ChangeDiff.objects.filter(branch=obj.branch).count(),
        permission="ipfabric_netbox.view_ipfabricingestion",
    )

    def get_children(self, request, parent):
        return self.child_model.objects.filter(branch=parent.branch)


@register_model_view(IPFabricIngestion, "ingestion_issues")
class IPFabricIngestionIssuesView(generic.ObjectChildrenView):
    queryset = IPFabricIngestion.objects.all()
    child_model = IPFabricIngestionIssue
    table = IPFabricIngestionIssuesTable
    template_name = "generic/object_children.html"
    filterset = IPFabricIngestionIssueFilterSet
    actions = (AddObject, BulkDelete)
    tab = ViewTab(
        label=_("Ingestion Issues"),
        badge=lambda obj: IPFabricIngestionIssue.objects.filter(ingestion=obj).count(),
        permission="ipfabric_netbox.view_ipfabricingestionissue",
    )

    def get_children(self, request, parent):
        return IPFabricIngestionIssue.objects.filter(ingestion=parent)


@register_model_view(IPFabricIngestion, "delete")
class IPFabricIngestionDeleteView(generic.ObjectDeleteView):
    queryset = IPFabricIngestion.objects.all()


@register_model_view(IPFabricIngestion, "bulk_delete", path="delete", detail=False)
class IPFabricIngestionBulkDeleteView(generic.BulkDeleteView):
    queryset = IPFabricIngestion.objects.all()
    table = IPFabricIngestionTable


@register_model_view(Device, "ipfabric")
class IPFabricTable(generic.ObjectView):
    template_name = "ipfabric_netbox/ipfabric_table.html"
    tab = ViewTab("IP Fabric", permission="ipfabric_netbox.view_devicetable")
    queryset = Device.objects.all()

    def get_extra_context(self, request, instance):
        """Process form and prepare table data for the template."""
        device = instance
        form = (
            IPFabricTableForm(request.GET)
            if "table" in request.GET
            else IPFabricTableForm()
        )
        restrict_form_fields(form, request.user)
        data = None
        source = None

        if form.is_valid():
            table_name = form.cleaned_data["table"]
            test = {
                "True": True,
                "False": False,
            }
            cache_enable = test.get(form.cleaned_data["cache_enable"])
            source = form.cleaned_data.get("source")

            if not form.cleaned_data["snapshot_data"]:
                snapshot_id = "$last"
                source = (
                    source
                    or IPFabricSource.objects.filter(
                        pk=device.custom_field_data.get("ipfabric_source")
                    ).first()
                    or IPFabricSource.get_for_site(device.site).first()
                )
            else:
                snapshot_id = form.cleaned_data["snapshot_data"].snapshot_id
                source = source or form.cleaned_data["snapshot_data"].source

            if source is not None:
                source.parameters["snapshot_id"] = snapshot_id
                source.parameters["base_url"] = source.url

                cache_key = f"ipfabric_{table_name}_{device.serial}_{source.parameters['snapshot_id']}"
                if cache_enable:
                    data = cache.get(cache_key)

                if not data:
                    try:
                        ipf = IPFabric(parameters=source.parameters)
                        raw_data, columns = ipf.get_table_data(
                            table=table_name, device=device
                        )
                        data = {"data": raw_data, "columns": columns}
                        cache.set(cache_key, data, 60 * 60 * 24)
                    except Exception as e:
                        messages.error(request, e)

        if not data:
            data = {"data": [], "columns": []}

        table = DeviceIPFTable(data["data"], extra_columns=data["columns"])

        RequestConfig(
            request,
            {
                "paginator_class": EnhancedPaginator,
                "per_page": get_paginate_count(request),
            },
        ).configure(table)

        if not source:
            if source_id := device.custom_field_data.get("ipfabric_source"):
                source = IPFabricSource.objects.filter(pk=source_id).first()
            else:
                source = IPFabricSource.get_for_site(device.site).first()

        return {
            "source": source,
            "form": form,
            "table": table,
        }

    def get(self, request, **kwargs):
        """Handle GET requests, with special handling for HTMX table updates."""
        # For HTMX requests, we only need to return the table HTML
        if request.htmx:
            device = get_object_or_404(Device, pk=kwargs.get("pk"))
            context = self.get_extra_context(request, device)
            return render(
                request,
                "htmx/table.html",
                {
                    "table": context["table"],
                },
            )

        # For regular requests, use the parent's get() method which will call get_extra_context()
        return super().get(request, **kwargs)


# endregion


# region - Filter
@register_model_view(IPFabricFilter, "list", path="", detail=False)
class IPFabricFilterListView(generic.ObjectListView):
    queryset = IPFabricFilter.objects.all()
    table = IPFabricFilterTable
    filterset = IPFabricFilterFilterSet
    template_name = "ipfabric_netbox/htmx_list.html"


@register_model_view(IPFabricFilter, "add", detail=False)
@register_model_view(IPFabricFilter, "edit")
class IPFabricFilterEditView(generic.ObjectEditView):
    queryset = IPFabricFilter.objects.all()
    form = IPFabricFilterForm
    default_return_url = "plugins:ipfabric_netbox:ipfabricfilter_list"


@register_model_view(IPFabricFilter, "delete")
class IPFabricFilterDeleteView(generic.ObjectDeleteView):
    queryset = IPFabricFilter.objects.all()
    default_return_url = "plugins:ipfabric_netbox:ipfabricfilter_list"


@register_model_view(IPFabricFilter, "bulk_import", path="import", detail=False)
class IPFabricFilterBulkImportView(generic.BulkImportView):
    queryset = IPFabricFilter.objects.all()
    model_form = IPFabricFilterBulkImportForm


@register_model_view(IPFabricFilter, "bulk_edit", path="edit", detail=False)
class IPFabricFilterBulkEditView(generic.BulkEditView):
    queryset = IPFabricFilter.objects.all()
    table = IPFabricFilterTable
    form = IPFabricFilterBulkEditForm


@register_model_view(IPFabricFilter, "bulk_rename", path="rename", detail=False)
class IPFabricFilterBulkRenameView(generic.BulkRenameView):
    queryset = IPFabricFilter.objects.all()


@register_model_view(IPFabricFilter, "bulk_delete", path="delete", detail=False)
class IPFabricFilterBulkDeleteView(generic.BulkDeleteView):
    queryset = IPFabricFilter.objects.all()
    table = IPFabricFilterTable


@register_model_view(IPFabricFilter)
class IPFabricFilterView(GetRelatedModelsMixin, generic.ObjectView):
    queryset = IPFabricFilter.objects.all()

    def get_extra_context(self, request, instance):
        return {
            "related_models": self.get_related_models(
                request,
                instance,
                extra=(
                    (
                        IPFabricSync.objects.restrict(request.user, "view").filter(
                            filters__in=[instance]
                        ),
                        "filter_id",
                    ),
                    (
                        IPFabricEndpoint.objects.restrict(request.user, "view").filter(
                            filters__in=[instance]
                        ),
                        "ipfabric_filter_id",
                    ),
                    (
                        IPFabricFilterExpression.objects.restrict(
                            request.user, "view"
                        ).filter(filters__in=[instance]),
                        "ipfabric_filter_id",
                    ),
                ),
            ),
        }


@register_model_view(
    IPFabricFilter,
    name="combined_expressions",
    path="combined-expressions",
    kwargs={"model": IPFabricFilter},
)
class IPFabricFilterCombinedExpressionsView(CombinedExpressionsBaseView):
    """Display merged expressions for a single filter."""

    def get_context_data(self, request, **kwargs):
        """Provide context for filter's merged expressions.

        Args:
            request: HTTP request (must be HTMX)
            **kwargs: Must contain 'pk' (filter primary key)

        Returns:
            dict: Standardized context with filter object and merged expressions
        """
        filter_pk = kwargs.get("pk")

        # Return empty context if not HTMX or missing required parameters
        if not request.htmx or not filter_pk:
            return {
                "object": None,
                "combined_expressions": [],
                "is_empty": True,
                "context_type": "filter",
            }

        # Force fresh query from database, prefetch expressions
        filter_obj = IPFabricFilter.objects.prefetch_related("expressions").get(
            pk=filter_pk
        )
        combined = filter_obj.merge_expressions()

        return {
            "object": filter_obj,
            "combined_expressions": combined,
            "is_empty": len(combined) == 0,
            "context_type": "filter",
        }


# endregion


# region - Filter Expression
@register_model_view(IPFabricFilterExpression, "list", path="", detail=False)
class IPFabricFilterExpressionListView(generic.ObjectListView):
    queryset = IPFabricFilterExpression.objects.all()
    table = IPFabricFilterExpressionTable
    filterset = IPFabricFilterExpressionFilterSet


@register_model_view(IPFabricFilterExpression, "add", detail=False)
@register_model_view(IPFabricFilterExpression, "edit")
class IPFabricFilterExpressionEditView(generic.ObjectEditView):
    queryset = IPFabricFilterExpression.objects.all()
    form = IPFabricFilterExpressionForm
    default_return_url = "plugins:ipfabric_netbox:ipfabricfilterexpression_list"
    template_name = "ipfabric_netbox/ipfabricfilterexpression_edit.html"

    def get_extra_context(self, request, instance):
        """Add test configuration context to the template."""
        context = super().get_extra_context(request, instance)

        # Add test-related context if editing existing object
        if instance.pk:
            # Get available test sources and endpoints
            sources, endpoints = get_filter_expression_test_candidates(instance)
            context["available_test_sources"] = list(sources)
            context["available_test_endpoints"] = list(endpoints)
            context["can_test"] = bool(sources and endpoints)
        else:
            context["can_test"] = False

        return context


@register_model_view(IPFabricFilterExpression, "delete")
class IPFabricFilterExpressionDeleteView(generic.ObjectDeleteView):
    queryset = IPFabricFilterExpression.objects.all()
    default_return_url = "plugins:ipfabric_netbox:ipfabricfilterexpression_list"


@register_model_view(
    IPFabricFilterExpression, "bulk_import", path="import", detail=False
)
class IPFabricFilterExpressionBulkImportView(generic.BulkImportView):
    queryset = IPFabricFilterExpression.objects.all()
    model_form = IPFabricFilterExpressionBulkImportForm


@register_model_view(IPFabricFilterExpression, "bulk_edit", path="edit", detail=False)
class IPFabricFilterExpressionBulkEditView(generic.BulkEditView):
    queryset = IPFabricFilterExpression.objects.all()
    table = IPFabricFilterExpressionTable
    form = IPFabricFilterExpressionBulkEditForm


@register_model_view(
    IPFabricFilterExpression, "bulk_rename", path="rename", detail=False
)
class IPFabricFilterExpressionBulkRenameView(generic.BulkRenameView):
    queryset = IPFabricFilterExpression.objects.all()


@register_model_view(
    IPFabricFilterExpression, "bulk_delete", path="delete", detail=False
)
class IPFabricFilterExpressionBulkDeleteView(generic.BulkDeleteView):
    queryset = IPFabricFilterExpression.objects.all()
    table = IPFabricFilterExpressionTable


@register_model_view(IPFabricFilterExpression)
class IPFabricFilterExpressionView(GetRelatedModelsMixin, generic.ObjectView):
    queryset = IPFabricFilterExpression.objects.all()

    def get_extra_context(self, request, instance):
        return {
            "related_models": self.get_related_models(
                request,
                instance,
                extra=(
                    (
                        IPFabricFilter.objects.restrict(request.user, "view").filter(
                            expressions__in=[instance]
                        ),
                        "expression_id",
                    ),
                ),
            ),
        }


@register_model_view(IPFabricFilterExpression, "test")
class IPFabricFilterExpressionTestView(View):
    """Test a filter expression against IP Fabric API."""

    def post(self, request, pk=None):
        # Get test parameters from POST data
        test_source_id = request.POST.get("test_source")
        test_endpoint_id = request.POST.get("test_endpoint")
        expression_json = request.POST.get("expression")

        return self._test_expression_logic(
            request, test_source_id, test_endpoint_id, expression_json
        )

    @staticmethod
    def _test_expression_logic(
        request, test_source_id, test_endpoint_id, expression_json
    ):
        """Shared logic for testing filter expressions (works for both saved and unsaved)."""

        # Validate required fields
        if not test_source_id:
            return HttpResponse(
                json.dumps({"success": False, "error": "Please select a Test Source."}),
                content_type="application/json",
            )

        if not test_endpoint_id:
            return HttpResponse(
                json.dumps(
                    {"success": False, "error": "Please select a Test Endpoint."}
                ),
                content_type="application/json",
            )

        # Validate expression JSON
        if expression_json:
            try:
                expression_data = json.loads(expression_json)
                if not isinstance(expression_data, list):
                    return HttpResponse(
                        json.dumps(
                            {
                                "success": False,
                                "error": "Expression must be a JSON list.",
                            }
                        ),
                        content_type="application/json",
                    )
                for idx, item in enumerate(expression_data):
                    if not isinstance(item, dict):
                        return HttpResponse(
                            json.dumps(
                                {
                                    "success": False,
                                    "error": f"Expression item at index {idx} must be a dictionary.",
                                }
                            ),
                            content_type="application/json",
                        )
            except json.JSONDecodeError as e:
                return HttpResponse(
                    json.dumps({"success": False, "error": f"Invalid JSON: {str(e)}"}),
                    content_type="application/json",
                )
        else:
            return HttpResponse(
                json.dumps({"success": False, "error": "Expression is required."}),
                content_type="application/json",
            )

        # Get source and endpoint objects
        try:
            source = IPFabricSource.objects.get(pk=test_source_id)
        except IPFabricSource.DoesNotExist:
            return HttpResponse(
                json.dumps({"success": False, "error": "Selected source not found."}),
                content_type="application/json",
                status=404,
            )

        # Check if source is LOCAL
        if source.type != IPFabricSourceTypeChoices.LOCAL:
            return HttpResponse(
                json.dumps(
                    {
                        "success": False,
                        "error": "Cannot test against REMOTE sources. Please select a LOCAL IP Fabric source with direct API access.",
                    }
                ),
                content_type="application/json",
            )

        try:
            endpoint = IPFabricEndpoint.objects.get(pk=test_endpoint_id)
        except IPFabricEndpoint.DoesNotExist:
            return HttpResponse(
                json.dumps({"success": False, "error": "Selected endpoint not found."}),
                content_type="application/json",
                status=404,
            )

        # Test the expression against IP Fabric API
        try:
            # Check that source has auth token
            auth = source.parameters.get("auth")
            if not auth:
                return HttpResponse(
                    json.dumps(
                        {
                            "success": False,
                            "error": "Source is missing API authentication token.",
                        }
                    ),
                    content_type="application/json",
                )

            # Prepare parameters for IPFabric client (same pattern as other views)
            parameters = source.parameters.copy()
            parameters.update(
                {
                    "snapshot_id": "$last",
                    "base_url": source.url,
                    "pagination": {"limit": 1, "start": 0},
                }
            )
            # Build filter - use AND type as default (IPF doesn't distinguish between types at this level)
            filter_dict = {"and": expression_data}
            ipf = IPFabric(parameters=parameters)
            results = ipf.ipf.fetch_all(
                url=endpoint.endpoint, snapshot_id="$last", filters=filter_dict
            )

            # Return success
            return HttpResponse(
                json.dumps(
                    {
                        "success": True,
                        "count": len(results),
                        "message": f"Test successful! Expression matched {len(results)} result(s) (limited to 1 for testing).",
                    }
                ),
                content_type="application/json",
            )

        except Exception as e:
            # Return error with helpful context
            error_msg = str(e)

            # Enhance error message for common issues
            if "Unrecognized key(s)" in error_msg:
                error_msg += f" | Hint: The field name in your expression doesn't exist in the '{endpoint.endpoint}' endpoint. Check the IP Fabric API documentation for valid field names for this table."
            elif "Invalid Input" in error_msg or "VALIDATION_FAILED" in error_msg:
                error_msg += f" | Hint: Check that your filter syntax is correct and field names are valid for endpoint '{endpoint.endpoint}'."

            return HttpResponse(
                json.dumps({"success": False, "error": f"API Error: {error_msg}"}),
                content_type="application/json",
            )


# endregion
