"""
Views for reconciling IP addresses, prefixes, and interfaces.

This module handles the reconciliation process between Slurpit data and NetBox:
- Listing and comparing Slurpit IP addresses, prefixes, and interfaces with NetBox
- Accepting or declining reconciliation changes
- Bulk operations for reconciling multiple items
- Detailed diff views showing what will change

The reconciliation process allows users to review differences between Slurpit data
and existing NetBox objects before applying changes.
"""

from django.contrib.contenttypes.models import ContentType
from netbox.views import generic
from django.http import JsonResponse

from ..models import SlurpitInitIPAddress, SlurpitInterface, SlurpitPrefix
from .. import forms, importer, models, tables
from ..decorators import slurpit_plugin_registered
from django.utils.decorators import method_decorator
from django.shortcuts import render
from ipam.models import VRF, IPAddress, Prefix
from utilities.data import shallow_compare_dict
from ..references.generic import normalize_ip_address
from django.shortcuts import redirect
from django.urls import reverse
from django.contrib import messages
from ..management.choices import *
from ..importer import BATCH_SIZE
from django.db import transaction
from dcim.models import Interface
from urllib.parse import urlencode
from ..filtersets import SlurpitIPAddressFilterSet, SlurpitInterfaceFilterSet, SlurpitPrefixFilterSet
from utilities.views import register_model_view
from ..forms import SlurpitPrefixForm, SlurpitDeviceInterfaceForm, SlurpitInitIPAMForm

class SlurpitInitIPAddressListView(generic.ObjectListView):
    """List view for Slurpit IP addresses awaiting reconciliation."""
    queryset = SlurpitInitIPAddress.objects.all()
    table = tables.SlurpitIPAMTable

class SlurpitPrefixListView(generic.ObjectListView):
    """List view for Slurpit prefixes awaiting reconciliation."""
    queryset = SlurpitPrefix.objects.all()
    table = tables.SlurpitPrefixTable

class SlurpitInterfaceListView(generic.ObjectListView):
    """List view for Slurpit interfaces awaiting reconciliation."""
    queryset = SlurpitInterface.objects.all()
    table = tables.SlurpitInterfaceTable


@method_decorator(slurpit_plugin_registered, name='dispatch')
class ReconcileView(generic.ObjectListView):
    """Main reconciliation view for IP addresses, prefixes, and interfaces.
    
    This view displays items that need reconciliation and allows users to:
    - View differences between Slurpit data and NetBox objects
    - Accept changes (create new or update existing NetBox objects)
    - Decline changes (remove Slurpit items without applying)
    
    The view switches between different object types based on the 'tab' query parameter:
    - 'ipam' or None: IP addresses
    - 'prefix': Prefixes
    - 'interface': Interfaces
    """
    queryset = models.SlurpitInitIPAddress.objects.exclude(address = None)
    table = tables.SlurpitIPAMTable
    template_name = "slurpit_netbox/reconcile.html"
    filterset = SlurpitIPAddressFilterSet

    def get(self, request, *args, **kwargs):
        """Handle GET requests and switch queryset/table based on tab parameter.
        
        Args:
            request: HTTP request object with optional 'tab' query parameter
            *args: Variable positional arguments
            **kwargs: Variable keyword arguments
        
        Returns:
            Rendered template with appropriate queryset and table
        """
        tab = request.GET.get('tab')
        # Default to IPAM tab if tab is missing or explicitly 'ipam'
        if tab == 'prefix':
            self.queryset = models.SlurpitPrefix.objects.exclude(prefix = None)
            self.table = tables.SlurpitPrefixTable
            self.filterset = SlurpitPrefixFilterSet
        elif tab == 'interface':
            # Interface tab
            self.queryset = models.SlurpitInterface.objects.exclude(name = '')
            self.table = tables.SlurpitInterfaceTable
            self.filterset = SlurpitInterfaceFilterSet
        else:
            # Default to IPAM tab (for None, 'ipam', or any other value)
            pass

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

    def get_extra_context(self, request, *args, **kwargs):
        """Get extra context data for the template.
        
        If a 'pk' parameter is provided, calculates differences between Slurpit data
        and existing NetBox objects for display in the reconciliation interface.
        
        Args:
            request: HTTP request object with optional 'pk' and 'tab' query parameters
            *args: Variable positional arguments
            **kwargs: Variable keyword arguments
        
        Returns:
            Dictionary containing:
            - object: NetBox object instance (if exists)
            - title: Display title for the object
            - diff_added: Dictionary of fields that will be added/changed
            - diff_removed: Dictionary of fields that will be removed/changed
            - incoming_change: Dictionary of incoming Slurpit data
            - current_state: Dictionary of current NetBox state
            - updated_time: Last update time from Slurpit
            - action: 'Created' or 'Updated'
            - object_type: Type of object being reconciled
            - Counts for each object type
            - edit_bulk_url: URL for bulk edit operations
        """
        reconcile_type = request.GET.get('tab')
        if not reconcile_type or reconcile_type not in ['ipam', 'prefix', 'interface']:
            reconcile_type = 'ipam'
        pk = request.GET.get('pk')
        return_values = {}
        title = ""

        # If pk is provided, calculate differences for detail view
        if pk:
            # Handle interface reconciliation
            if reconcile_type == 'interface':
                self.queryset = models.SlurpitInterface.objects.all()
                diff_added = None
                diff_removed = None
                action = 'Updated'

                # Get fields for lookup (excluding those not on Interface)
                interface_fields = ['name', 'device', 'label', 'module', 'type', 'speed', 'duplex', 'enabled', 'description', 'address', 'vrf']
                interface_db_fields = ['name', 'label', 'device', 'module', 'type', 'duplex', 'speed', 'enabled', 'description']
                
                # Get incoming Slurpit interface data
                incomming_queryset = SlurpitInterface.objects.filter(pk=pk)
                incomming_obj_values = incomming_queryset.values(*SlurpitInterface.reconcile_fields).first()

                name = str(incomming_queryset.first().name)
                updated_time = incomming_queryset.first().last_updated
                title = name
                device = incomming_obj_values['device']

                incomming_obj_values['name'] = name
                if 'ip_address' in incomming_obj_values:
                    val = incomming_obj_values.pop('ip_address')
                    incomming_obj_values['address'] = normalize_ip_address(val, Prefix) if val else None

                incomming_change = {f: incomming_obj_values.get(f) for f in interface_fields if f in incomming_obj_values}

                # Check if interface already exists in NetBox
                current_queryset = Interface.objects.filter(name=name, device=device)

                if current_queryset:
                    # Interface exists - will be updated
                    instance = current_queryset.first()
                    current_obj = current_queryset.values(*interface_db_fields).first()
                    current_obj['name'] = name
                    
                    # Manually add address and vrf for comparison
                    first_ip = instance.ip_addresses.first()
                    current_obj['address'] = str(first_ip.address) if first_ip else None
                    current_obj['vrf'] = first_ip.vrf.pk if first_ip and first_ip.vrf else None
                    
                    current_state = {f: current_obj.get(f) for f in interface_fields if f in current_obj}
                else:
                    # Interface doesn't exist - will be created
                    current_state = None
                    instance = None
                    action = 'Created'
                

                # Calculate differences if both states exist
                if current_state and incomming_change:
                    diff_added = shallow_compare_dict(
                        current_state or dict(),
                        incomming_change or dict(),
                        exclude=['last_updated', 'vrf'],
                    )
                    # Get removed/changed values from current state
                    diff_removed = {
                        x: current_state.get(x) for x in diff_added
                    } if current_state else {}
                else:
                    diff_added = None
                    diff_removed = None

                object_type = f'{Interface._meta.app_label} | {Interface._meta.verbose_name}'
            
            # Handle prefix reconciliation
            elif reconcile_type == 'prefix':
                self.queryset = models.SlurpitPrefix.objects.all()
                diff_added = None
                diff_removed = None
                action = 'Updated'

                # Get ignore settings to determine if VRF should be ignored
                initial_obj = SlurpitPrefix.objects.filter(prefix=None).values(*SlurpitPrefix.ignore_fields).first()
                ignore_vrf = False
                if initial_obj and initial_obj.get('ignore_vrf'):
                    ignore_vrf = True

                # Get incoming Slurpit prefix data
                incomming_queryset = SlurpitPrefix.objects.filter(pk=pk)
                prefix_fields = ['prefix', *SlurpitPrefix.reconcile_fields]
                incomming_obj = incomming_queryset.values(*prefix_fields).first()
                incomming_obj['prefix'] = str(incomming_queryset.first().prefix)
                incomming_change = {f: incomming_obj.get(f) for f in prefix_fields if f in incomming_obj}

                # Remove VRF from change dict if ignore_vrf is set
                if ignore_vrf and 'vrf' in incomming_change:
                    incomming_change.pop('vrf')

                prefix = str(incomming_queryset.first().prefix)
                updated_time = incomming_queryset.first().last_updated
                title = prefix
                vrf = None

                # Get VRF if specified
                if incomming_obj['vrf'] is not None:
                    vrf = VRF.objects.get(pk=incomming_obj['vrf'])
                
                # Check if prefix already exists in NetBox
                # If ignore_vrf is set, search by prefix only (ignore VRF)
                if ignore_vrf:
                    current_queryset = Prefix.objects.filter(prefix=prefix)
                else:
                    current_queryset = Prefix.objects.filter(prefix=prefix, vrf=vrf)

                if current_queryset:
                    # Prefix exists - will be updated
                    current_obj = current_queryset.values(*prefix_fields).first()
                    current_obj['prefix'] = str(current_obj.get('prefix'))
                    current_state = {f: current_obj.get(f) for f in prefix_fields if f in current_obj}
                    
                    # Remove VRF from state dict if ignore_vrf is set
                    if ignore_vrf and 'vrf' in current_state:
                        current_state.pop('vrf')

                    instance = current_queryset.first()
                else:
                    # Prefix doesn't exist - will be created
                    current_state = None
                    instance = None
                    action = 'Created'
                
                # Calculate differences if both states exist
                if current_state and incomming_change:
                    # Exclude VRF from diff if ignore_vrf is set
                    exclude_fields = ['last_updated']
                    if ignore_vrf:
                        exclude_fields.append('vrf')

                    diff_added = shallow_compare_dict(
                        current_state or dict(),
                        incomming_change or dict(),
                        exclude=exclude_fields,
                    )
                    # Get removed/changed values from current state
                    diff_removed = {
                        x: current_state.get(x) for x in diff_added
                    } if current_state else {}
                else:
                    diff_added = None
                    diff_removed = None


                object_type = f'{Prefix._meta.app_label} | {Prefix._meta.verbose_name}'

            # Handle IP address reconciliation (default)
            else:
                diff_added = None
                diff_removed = None
                action = 'Updated'

                # Get fields to compare (address + reconcile_fields)
                ipam_fields = ('address', *SlurpitInitIPAddress.reconcile_fields)

                # Get ignore settings to determine if VRF should be ignored
                initial_obj = SlurpitInitIPAddress.objects.filter(address=None).values(*SlurpitInitIPAddress.ignore_fields).first()
                ignore_vrf = False
                if initial_obj and initial_obj.get('ignore_vrf'):
                    ignore_vrf = True

                # Get incoming Slurpit IP address data (always include VRF for lookup)
                incomming_queryset = SlurpitInitIPAddress.objects.filter(pk=pk)
                incomming_obj = incomming_queryset.values(*ipam_fields).first()

                ipaddress = str(incomming_queryset.first().address)
                ipaddress_stripped = str(incomming_queryset.first().address.ip)  # IP without CIDR
                updated_time = incomming_queryset.first().last_updated

                title = ipaddress
                vrf = incomming_obj.get('vrf')
                
                incomming_obj['address'] = ipaddress
                incomming_change = {f: incomming_obj.get(f) for f in ipam_fields if f in incomming_obj}
                
                # Remove VRF from change dict if ignore_vrf is set
                if ignore_vrf and 'vrf' in incomming_change:
                    incomming_change.pop('vrf')

                # Check if IP address already exists in NetBox
                # If ignore_vrf is set, search by address only (ignore VRF)
                if ignore_vrf:
                    current_queryset = IPAddress.objects.filter(address__net_host=ipaddress_stripped)
                else:
                    current_queryset = IPAddress.objects.filter(address__net_host=ipaddress_stripped, vrf=vrf)
                if current_queryset:
                    # IP address exists - will be updated
                    current_obj = current_queryset.values(*ipam_fields).first()
                    current_obj['address'] = str(current_obj.get('address'))
                    current_state = {f: current_obj.get(f) for f in ipam_fields if f in current_obj}
                    
                    # Remove VRF from state dict if ignore_vrf is set
                    if ignore_vrf and 'vrf' in current_state:
                        current_state.pop('vrf')
                    
                    instance = current_queryset.first()
                else:
                    # IP address doesn't exist - will be created
                    current_state = None
                    instance = None
                    action = 'Created'
                

                # Calculate differences if both states exist
                if current_state and incomming_change:
                    # Exclude VRF from diff if ignore_vrf is set (already removed from dicts, but add to exclude for safety)
                    exclude_fields = ['last_updated']
                    if ignore_vrf:
                        exclude_fields.append('vrf')
                    
                    diff_added = shallow_compare_dict(
                        current_state or dict(),
                        incomming_change or dict(),
                        exclude=exclude_fields,
                    )
                    # Get removed/changed values from current state
                    diff_removed = {
                        x: current_state.get(x) for x in diff_added
                    } if current_state else {}
                else:
                    diff_added = None
                    diff_removed = None

                object_type = f'{IPAddress._meta.app_label} | {IPAddress._meta.verbose_name}'

            return_values = {
                'object': instance,
                'title': title,
                'diff_added': diff_added,
                'diff_removed': diff_removed,
                'incomming_change': incomming_change,
                'current_state': current_state,
                'updated_time': updated_time,
                'action': action,
                'object_type': object_type
            }

        # Set bulk edit URL based on reconcile type
        if reconcile_type == 'interface':
            edit_bulk_url = reverse("plugins:slurpit_netbox:slurpitinterface_bulk_edit")
        elif reconcile_type == 'prefix':
            edit_bulk_url = reverse("plugins:slurpit_netbox:slurpitprefix_bulk_edit")
        else:
            edit_bulk_url = reverse("plugins:slurpit_netbox:slurpitipaddress_bulk_edit")
        
        # Add counts and bulk edit URL to return values
        return_values = {
            **return_values,
            'ipam_count': models.SlurpitInitIPAddress.objects.exclude(address = None).count(),
            'interface_count': models.SlurpitInterface.objects.exclude(name = '').count(),
            'prefix_count': models.SlurpitPrefix.objects.exclude(prefix = None).count(),
            'edit_bulk_url': edit_bulk_url
        }

        return return_values
    
    def post(self, request, **kwargs):
        """Handle POST requests for reconciliation operations.
        
        Supports multiple actions:
        - 'get': Get difference details for a single object (returns JSON)
        - 'decline': Delete selected Slurpit items without applying changes
        - Accept (default): Apply changes to NetBox (create or update objects)
        
        Args:
            request: HTTP request object with POST data
            **kwargs: Variable keyword arguments
        
        Returns:
            JSON response for 'get' action, redirect for other actions
        """
        pk_list = request.POST.getlist('pk')
        action = request.POST.get('action')
        tab = request.POST.get('tab')
        if not tab:
            tab = 'ipam'
        _all = request.POST.get('_all')

        # Get action: Return JSON with difference details for a single object
        if action == 'get':
            pk = request.POST.get('pk')

            # Handle interface difference calculation
            if tab == 'interface':
                self.queryset = models.SlurpitInterface.objects.all()
                diff_added = None
                diff_removed = None
                action = 'Updated'

                interface_fields = ['name', 'device', 'label', 'module', 'type', 'speed', 'duplex', 'enabled', 'description', 'address', 'vrf']

                initial_obj = SlurpitInterface.objects.filter(name='').values(
                    'ignore_module', 'ignore_type', 'ignore_speed', 'ignore_duplex'
                ).first()

                interface_update_ignore_values = []
                if initial_obj:
                    for key, val in initial_obj.items():
                        if val:
                            interface_update_ignore_values.append(key)

                fields_to_remove = []
                for field in ['module', 'type', 'speed', 'duplex']:
                    if f'ignore_{field}' in interface_update_ignore_values:
                        fields_to_remove.append(field)

                # Final list of fields to compare (excluding ignored ones)
                interface_fields = [f for f in interface_fields if f not in fields_to_remove]

                # Database fields present on dcim.Interface
                interface_db_fields = ['name', 'label', 'device', 'module', 'type', 'duplex', 'speed', 'enabled', 'description']

                incomming_queryset = SlurpitInterface.objects.filter(pk=pk)
                incomming_obj_values = incomming_queryset.values(*SlurpitInterface.reconcile_fields).first()

                name = str(incomming_queryset.first().name)
                updated_time = incomming_queryset.first().last_updated
                title = name
                device = incomming_obj_values['device']

                incomming_obj_values['name'] = name
                if 'ip_address' in incomming_obj_values:
                    val = incomming_obj_values.pop('ip_address')
                    incomming_obj_values['address'] = normalize_ip_address(val, Prefix) if val else None

                incomming_change = {f: incomming_obj_values.get(f) for f in interface_fields if f in incomming_obj_values}

                current_queryset = Interface.objects.filter(name=name, device=device)

                if current_queryset:
                    instance = current_queryset.first()
                    current_obj = current_queryset.values(*interface_db_fields).first()
                    current_obj['name'] = name
                    
                    # Manually add address and vrf for comparison
                    first_ip = instance.ip_addresses.first()
                    current_obj['address'] = str(first_ip.address) if first_ip else None
                    current_obj['vrf'] = first_ip.vrf.pk if first_ip and first_ip.vrf else None

                    current_state = {f: current_obj.get(f) for f in interface_fields if f in current_obj}
                else:
                    current_state = None
                    instance = None
                    action = 'Created'
                

                if current_state and incomming_change:
                    diff_added = shallow_compare_dict(
                        current_state or dict(),
                        incomming_change or dict(),
                        exclude=['last_updated', 'vrf'],
                    )
                    diff_removed = {
                        x: current_state.get(x) for x in diff_added
                    } if current_state else {}
                else:
                    diff_added = None
                    diff_removed = None

                object_type = f'{Interface._meta.app_label} | {Interface._meta.verbose_name}'
            
            elif tab == 'prefix':
                self.queryset = models.SlurpitPrefix.objects.all()
                diff_added = None
                diff_removed = None
                action = 'Updated'

                prefix_fields = ['prefix'] + list(SlurpitPrefix.reconcile_fields)

                initial_obj = SlurpitPrefix.objects.filter(prefix=None).values(
                    'ignore_status', 'ignore_vrf', 'ignore_role', 'ignore_site', 'ignore_vlan', 'ignore_tenant', 'ignore_description'
                ).first()

                prefix_update_ignore_values = []

                if initial_obj:
                    initial_prefix_values = {**initial_obj}

                    for key in initial_prefix_values.keys():
                        if initial_prefix_values[key]:
                            prefix_update_ignore_values.append(key)

                updated_fields = ['status', 'tenant', 'description', 'role', 'vlan']
                fields_to_remove = []
                
                for field in updated_fields:
                    ignore_field = f'ignore_{field}'
                    if ignore_field in prefix_update_ignore_values:
                        fields_to_remove.append(field)

                # Check if VRF should be ignored for lookup
                ignore_vrf = 'ignore_vrf' in prefix_update_ignore_values

                prefix_fields = [f for f in prefix_fields if f not in fields_to_remove]

                incomming_queryset = SlurpitPrefix.objects.filter(pk=pk)
                incomming_obj = incomming_queryset.values(*prefix_fields).first()
                incomming_obj['prefix'] = str(incomming_queryset.first().prefix)
                incomming_change = {f: incomming_obj.get(f) for f in prefix_fields if f in incomming_obj}

                # Remove VRF from change dict if ignore_vrf is set
                if ignore_vrf and 'vrf' in incomming_change:
                    incomming_change.pop('vrf')

                prefix = str(incomming_queryset.first().prefix)
                updated_time = incomming_queryset.first().last_updated
                title = prefix
                vrf = None

                if incomming_obj['vrf'] is not None:
                    vrf = VRF.objects.get(pk=incomming_obj['vrf'])
                
                # If ignore_vrf is set, search by prefix only (ignore VRF)
                if ignore_vrf:
                    current_queryset = Prefix.objects.filter(prefix=prefix)
                else:
                    current_queryset = Prefix.objects.filter(prefix=prefix, vrf=vrf)

                if current_queryset:
                    current_obj = current_queryset.values(*prefix_fields).first()
                    current_obj['prefix'] = str(current_obj['prefix'])
                    current_state = {f: current_obj.get(f) for f in prefix_fields if f in current_obj}
                    
                    # Remove VRF from state dict if ignore_vrf is set
                    if ignore_vrf and 'vrf' in current_state:
                        current_state.pop('vrf')

                    instance = current_queryset.first()
                else:
                    current_state = None
                    instance = None
                    action = 'Created'
                
                if current_state and incomming_change:
                    # Exclude VRF from diff if ignore_vrf is set
                    exclude_fields = ['last_updated']
                    if ignore_vrf:
                        exclude_fields.append('vrf')

                    diff_added = shallow_compare_dict(
                        current_state or dict(),
                        incomming_change or dict(),
                        exclude=exclude_fields,
                    )
                    diff_removed = {
                        x: current_state.get(x) for x in diff_added
                    } if current_state else {}
                else:
                    diff_added = None
                    diff_removed = None


                object_type = f'{Prefix._meta.app_label} | {Prefix._meta.verbose_name}'

            else:
                diff_added = None
                diff_removed = None
                action = 'Updated'

                ipam_fields = ['address'] + list(SlurpitInitIPAddress.reconcile_fields)

                initial_obj = SlurpitInitIPAddress.objects.filter(address=None).values(
                    'ignore_status', 'ignore_vrf', 'ignore_tenant', 'ignore_role', 'ignore_description'
                ).first()

                ipaddress_update_ignore_values = []

                if initial_obj:
                    initial_ipaddress_values = {**initial_obj}

                    for key in initial_ipaddress_values.keys():
                        if initial_ipaddress_values[key]:
                            ipaddress_update_ignore_values.append(key)

                updated_fields = ['status', 'role', 'tenant', 'dns_name', 'description', 'vrf']
                fields_to_remove = []
                
                for field in updated_fields:
                    ignore_field = f'ignore_{field}'
                    if ignore_field in ipaddress_update_ignore_values:
                        fields_to_remove.append(field)

                # Check if VRF should be ignored for lookup
                ignore_vrf = 'ignore_vrf' in ipaddress_update_ignore_values

                # Always include VRF in query fields (needed for lookup), but remove from diff fields
                ipam_fields_for_query = ipam_fields.copy()  # Keep VRF for query
                ipam_fields_for_diff = [f for f in ipam_fields if f not in fields_to_remove]  # Remove ignored fields for diff

                incomming_queryset = SlurpitInitIPAddress.objects.filter(pk=pk)
                incomming_obj = incomming_queryset.values(*ipam_fields_for_query).first()

                ipaddress = str(incomming_queryset.first().address)
                ipaddress_stripped = str(incomming_queryset.first().address.ip)
                updated_time = incomming_queryset.first().last_updated

                title = ipaddress
                vrf = incomming_obj.get('vrf')  # Use .get() in case VRF was removed
                
                incomming_obj['address'] = ipaddress
                # Create change dict with only fields that should be in diff
                incomming_change = {f: incomming_obj.get(f) for f in ipam_fields_for_diff + ['address'] if f in incomming_obj}
                
                # If ignore_vrf is set, search by address only (ignore VRF)
                if ignore_vrf:
                    current_queryset = IPAddress.objects.filter(address__net_host=ipaddress_stripped)
                else:
                    current_queryset = IPAddress.objects.filter(address__net_host=ipaddress_stripped, vrf=vrf)
                
                if current_queryset:
                    current_obj = current_queryset.values(*ipam_fields_for_query).first()
                    current_obj['address'] = str(current_obj['address'])
                    # Create state dict with only fields that should be in diff
                    current_state = {f: current_obj.get(f) for f in ipam_fields_for_diff + ['address'] if f in current_obj}
                    instance = current_queryset.first()
                else:
                    current_state = None
                    instance = None
                    action = 'Created'
                

                if current_state and incomming_change:
                    # Exclude VRF from diff if ignore_vrf is set
                    exclude_fields = ['last_updated']
                    if ignore_vrf:
                        exclude_fields.append('vrf')
                    
                    diff_added = shallow_compare_dict(
                        current_state or dict(),
                        incomming_change or dict(),
                        exclude=exclude_fields,
                    )
                    diff_removed = {
                        x: current_state.get(x) for x in diff_added
                    } if current_state else {}
                else:
                    diff_added = None
                    diff_removed = None

                object_type = f'{IPAddress._meta.app_label} | {IPAddress._meta.verbose_name}'

            return_values = {
                'title': title,
                'diff_added': diff_added,
                'diff_removed': diff_removed,
                'incomming_change': incomming_change,
                'current_state': current_state,
                'updated_time': updated_time,
                'action': action,
                'object_type': object_type
            }

            return JsonResponse(return_values)
        
        # Process reconciliation if items are selected
        if _all or len(pk_list):
            # Decline action: Delete Slurpit items without applying changes
            if action == 'decline':
                try:
                    if tab == 'interface':
                        if _all:
                            models.SlurpitInterface.objects.exclude(name='').delete()
                        else:
                            models.SlurpitInterface.objects.filter(pk__in=pk_list).delete()
                        messages.info(request, "Declined the selected Interfaces successfully .")
                    elif tab == 'prefix':
                        if _all:
                            models.SlurpitPrefix.objects.exclude(prefix=None).delete()
                        else:
                            models.SlurpitPrefix.objects.filter(pk__in=pk_list).delete()
                        messages.info(request, "Declined the selected Prefixes successfully .")
                    else:
                        # IP address tab
                        if _all:
                            SlurpitInitIPAddress.objects.exclude(address=None).delete()
                        else:
                            SlurpitInitIPAddress.objects.filter(pk__in=pk_list).delete()
                        messages.info(request, "Declined the selected IP Addresses successfully .")
                except:
                    # Handle errors gracefully
                    if tab == 'interface':
                        messages.warning(request, "Failed to decline Interfaces.")
                    elif tab == 'prefix':
                        messages.warning(request, "Failed to decline Prefixes.")
                    else:
                        messages.warning(request, "Failed to decline IP Addresses.")
            else:
                # Accept action: Apply changes to NetBox (create or update)
                # Lists for batch operations
                batch_insert_qs = []  # New objects to create
                batch_insert_ids = []  # IDs of Slurpit items to delete after creation

                # Handle interface reconciliation
                if tab == 'interface':
                    # Get items to reconcile
                    if _all:
                        reconcile_items = SlurpitInterface.objects.exclude(name='')
                    else:
                        reconcile_items = SlurpitInterface.objects.filter(pk__in=pk_list)

                    # Get ignore settings from initial object
                    initial_obj = SlurpitInterface.objects.filter(name='').values(*SlurpitInterface.ignore_fields).first()
                    initial_interface_values = {}
                    interface_update_ignore_values = []

                    if initial_obj:
                        initial_interface_values = {**initial_obj}

                        # Collect fields that should be ignored during updates
                        for key in initial_interface_values.keys():
                            if initial_interface_values[key]:
                                interface_update_ignore_values.append(key)

                    # Determine which fields to update (excluding ignored ones)
                    updated_fields = SlurpitInterface.reconcile_fields
                    fields_to_remove = []
                    
                    for field in updated_fields:
                        ignore_field = f'ignore_{field}'
                        if ignore_field in interface_update_ignore_values:
                            fields_to_remove.append(field)

                    # Final list of fields to update
                    updated_fields = list(set(updated_fields) - set(fields_to_remove))

                    # Process each interface item
                    for item in reconcile_items:
                        # Check if interface already exists in NetBox
                        netbox_interface = Interface.objects.filter(name=item.name, device=item.device)
                        if netbox_interface:
                            # Update existing interface
                            netbox_interface = netbox_interface.first()

                            # Take snapshot for change logging if supported
                            if netbox_interface.pk and hasattr(netbox_interface, 'snapshot'):
                                netbox_interface.snapshot()
                    # Process each interface item
                    for item in reconcile_items:
                        with transaction.atomic():
                            # Check if interface already exists in NetBox
                            netbox_interface = Interface.objects.filter(name=item.name, device=item.device).first()
                            if not netbox_interface:
                                # Create new interface
                                netbox_interface = Interface(
                                    name = item.name,
                                    device = item.device,
                                    label = item.label,
                                    speed = item.speed,
                                    type = item.type,
                                    description = item.description,
                                    duplex = item.duplex,
                                    module = item.module,
                                    enabled = item.enabled
                                )
                            else:
                                # Update existing interface
                                if hasattr(netbox_interface, 'snapshot'):
                                    netbox_interface.snapshot()

                                # Update fields that are not ignored
                                for field_name in updated_fields:
                                    field_value = getattr(item, field_name, None)
                                    if field_value is not None and field_value != "":
                                        setattr(netbox_interface, field_name, field_value)

                            netbox_interface.save()

                            # Handle IP Address assignment
                            if item.ip_address:
                                import ipaddress
                                # Unassign existing IP addresses from the interface
                                interface_ct = ContentType.objects.get_for_model(Interface)
                                IPAddress.objects.filter(
                                    assigned_object_type=interface_ct,
                                    assigned_object_id=netbox_interface.pk
                                ).update(assigned_object_id=None, assigned_object_type=None)

                                # Find or create IP address in NetBox using host-based matching and ignoring VRF
                                try:
                                    ip_stripped = str(ipaddress.ip_interface(item.ip_address).ip)
                                    ip_obj = IPAddress.objects.filter(address__net_host=ip_stripped).first()
                                    
                                    if not ip_obj:
                                        # Create new IP with Slurpit's address and VRF
                                        ip_obj = IPAddress(address=item.ip_address, vrf=item.vrf)
                                    else:
                                        # Update existing IP with Slurpit's address (ensures mask is updated)
                                        ip_obj.address = item.ip_address
                                    
                                    ip_obj.assigned_object = netbox_interface
                                    ip_obj.status = 'active'
                                    ip_obj.save()

                                    # Cleanup Slurpit staging table for IPAM
                                    SlurpitInitIPAddress.objects.filter(address__net_host=ip_stripped).delete()
                                except Exception as e:
                                    # Log error but continue with other items
                                    print(f"Error assigning IP {item.ip_address} to interface {item.name}: {e}")

                            # Remove reconciled item from Slurpit staging
                            item.delete()
                # Handle prefix reconciliation
                elif tab == 'prefix':
                    # Get items to reconcile
                    if _all:
                        reconcile_items = SlurpitPrefix.objects.exclude(prefix=None)
                    else:
                        reconcile_items = SlurpitPrefix.objects.filter(pk__in=pk_list)
                    
                    # Get ignore settings from initial object
                    initial_obj = SlurpitPrefix.objects.filter(prefix=None).values(*SlurpitPrefix.ignore_fields).first()
                    initial_prefix_values = {}
                    prefix_update_ignore_values = []

                    if initial_obj:
                        initial_prefix_values = {**initial_obj}

                        # Collect fields that should be ignored during updates
                        for key in initial_prefix_values.keys():
                            if initial_prefix_values[key]:
                                prefix_update_ignore_values.append(key)

                    # Determine which fields to update (excluding ignored ones)
                    updated_fields = SlurpitPrefix.reconcile_fields
                    fields_to_remove = []
                    
                    for field in updated_fields:
                        ignore_field = f'ignore_{field}'
                        if ignore_field in prefix_update_ignore_values:
                            fields_to_remove.append(field)

                    # Check if VRF should be ignored for lookup
                    ignore_vrf = 'ignore_vrf' in prefix_update_ignore_values

                    # Final list of fields to update
                    updated_fields = [f for f in updated_fields if f not in fields_to_remove]

                    # Process each prefix item
                    for item in reconcile_items:
                        # Check if prefix already exists in NetBox
                        # If ignore_vrf is set, search by prefix only (ignore VRF)
                        if ignore_vrf:
                            netbox_prefix = Prefix.objects.filter(prefix=item.prefix).first()
                        else:
                            netbox_prefix = Prefix.objects.filter(prefix=item.prefix, vrf=item.vrf).first()
                        
                        if netbox_prefix:

                            # Take snapshot for change logging if supported
                            if netbox_prefix.pk and hasattr(netbox_prefix, 'snapshot'):
                                netbox_prefix.snapshot()

                            # Update fields that are not ignored
                            for field in item._meta.fields:
                                field_name = field.name
                                field_value = getattr(item, field_name)
                                if field_name in updated_fields and field_value is not None and field_value != "":
                                    setattr(netbox_prefix, field_name, field_value)

                            # Ensure description is not None
                            if item.description is None:
                                netbox_prefix.description = ""
                            
                            netbox_prefix.save()
                            # Remove reconciled item from Slurpit
                            SlurpitPrefix.objects.filter(pk=item.pk).delete()
                        else:
                            # Prefix doesn't exist - add to batch for creation
                            batch_insert_qs.append(
                                Prefix(
                                    prefix = item.prefix,
                                    status = item.status, 
                                    vrf = item.vrf,
                                    role = item.role, 
                                    vlan = item.vlan,
                                    description = item.description,
                                    tenant = item.tenant,
                            ))
                            batch_insert_ids.append(item.pk)
                        
                    # Batch create new prefixes
                    count = len(batch_insert_qs)
                    offset = 0
                    while offset < count:
                        batch_qs = batch_insert_qs[offset:offset + BATCH_SIZE]
                        batch_ids = batch_insert_ids[offset:offset + BATCH_SIZE]

                        # Create prefixes and delete Slurpit items in transaction
                        # Note: Prefix.save() is needed to trigger post_save signals
                        with transaction.atomic():
                            for prefix_item in batch_qs:
                                prefix_item.save()  # needs save to trigger the @receiver(post_save) signal
                            SlurpitPrefix.objects.filter(pk__in=batch_ids).delete()
                        offset += BATCH_SIZE
                # Handle IP address reconciliation (default)
                else:
                    # Get items to reconcile
                    if _all:
                        reconcile_items = SlurpitInitIPAddress.objects.exclude(address=None)
                    else:
                        reconcile_items = SlurpitInitIPAddress.objects.filter(pk__in=pk_list)

                    # Get ignore settings from initial object
                    initial_obj = SlurpitInitIPAddress.objects.filter(address=None).values(*SlurpitInitIPAddress.ignore_fields).first()

                    initial_ipaddress_values = {}
                    ipaddress_update_ignore_values = []

                    if initial_obj:
                        initial_ipaddress_values = {**initial_obj}

                        # Collect fields that should be ignored during updates
                        for key in initial_ipaddress_values.keys():
                            if initial_ipaddress_values[key]:
                                ipaddress_update_ignore_values.append(key)

                    # Determine which fields to update (excluding ignored ones)
                    updated_fields = SlurpitInitIPAddress.reconcile_fields
                    fields_to_remove = []
                    
                    for field in updated_fields:
                        ignore_field = f'ignore_{field}'
                        if ignore_field in ipaddress_update_ignore_values:
                            fields_to_remove.append(field)

                    # Final list of fields to update
                    updated_fields = list(set(updated_fields) - set(fields_to_remove))

                    # Process each IP address item
                    for item in reconcile_items:
                        address = str(item.address.ip)  # IP without CIDR
                        # Check if IP address already exists in NetBox
                        # If ignore_vrf is set, search by address only (ignore VRF)
                        if 'ignore_vrf' in ipaddress_update_ignore_values:
                            netbox_ipaddress = IPAddress.objects.filter(address__net_host=address)
                        else:
                            netbox_ipaddress = IPAddress.objects.filter(address__net_host=address, vrf=item.vrf)
                        if netbox_ipaddress:
                            # Update existing IP address
                            netbox_ipaddress = netbox_ipaddress.first()
                            netbox_ipaddress.address = item.address

                            # Take snapshot for change logging if supported
                            if netbox_ipaddress.pk and hasattr(netbox_ipaddress, 'snapshot'):
                                netbox_ipaddress.snapshot()

                            # Update fields that are not ignored
                            for field in item._meta.fields:
                                field_name = field.name
                                field_value = getattr(item, field_name)
                                if field_name in updated_fields and field_value is not None and field_value != "":
                                    setattr(netbox_ipaddress, field_name, field_value)

                            # Ensure dns_name and description are not None
                            if item.dns_name is None:
                                netbox_ipaddress.dns_name = ""

                            if item.description is None:
                                netbox_ipaddress.description = ""

                            netbox_ipaddress.save()
                            # Remove reconciled item from Slurpit
                            SlurpitInitIPAddress.objects.filter(pk=item.pk).delete()
                        else:
                            # IP address doesn't exist - add to batch for creation
                            batch_insert_qs.append(
                                IPAddress(
                                    address = item.address, 
                                    vrf = item.vrf,
                                    status = item.status, 
                                    role = item.role,
                                    description = item.description,
                                    tenant = item.tenant,
                                    dns_name = item.dns_name,
                            ))

                            batch_insert_ids.append(item.pk)
                        
                    # Batch create new IP addresses
                    count = len(batch_insert_qs)
                    offset = 0
                    while offset < count:
                        batch_qs = batch_insert_qs[offset:offset + BATCH_SIZE]
                        batch_ids = batch_insert_ids[offset:offset + BATCH_SIZE]
                        to_import = []        
                        for ipaddress_item in batch_qs:
                            to_import.append(ipaddress_item)

                        # Create IP addresses and delete Slurpit items in transaction
                        with transaction.atomic():
                            IPAddress.objects.bulk_create(to_import)
                            SlurpitInitIPAddress.objects.filter(pk__in=batch_ids).delete()
                        offset += BATCH_SIZE
        else:
            messages.warning(request, "No Reconcile Items were selected.")

        # Redirect back to reconcile list with appropriate tab
        if tab is None:
            tab = 'ipam'
        query_params = {'tab': tab}
        base_url = reverse("plugins:slurpit_netbox:reconcile_list")
        url_with_querystring = f"{base_url}?{urlencode(query_params)}"

        return redirect(url_with_querystring)

class ReconcileDetailView(generic.ObjectView):
    """Detail view for a single reconciliation item.
    
    Displays a detailed comparison between Slurpit data and NetBox object,
    showing what fields will be added, removed, or changed.
    """
    queryset = models.SlurpitInitIPAddress.objects.all()

    template_name = 'slurpit_netbox/reconcile_detail.html'

    def get(self, request, pk, reconcile_type, **kwargs):
        """Handle GET requests for reconciliation detail view.
        
        Calculates and displays differences between Slurpit data and NetBox object
        for a single item (interface, prefix, or IP address).
        
        Args:
            request: HTTP request object
            pk: Primary key of the Slurpit item to display
            reconcile_type: Type of object ('interface', 'prefix', or 'ipam')
            **kwargs: Variable keyword arguments
        
        Returns:
            Rendered template with difference details
        """
        title = ""
        if reconcile_type == 'interface':
            self.queryset = models.SlurpitInterface.objects.all()
            instance = self.get_object(pk=pk, **kwargs)
            diff_added = None
            diff_removed = None
            action = 'Updated'

            interface_fields = ['name', 'device', 'label', 'module', 'type', 'speed', 'duplex', 'enabled', 'description', 'address', 'vrf']
            netbox_fields = [f for f in interface_fields if f not in ['address', 'vrf']]

            incomming_queryset = SlurpitInterface.objects.filter(pk=pk)
            incomming_obj = incomming_queryset.values(*SlurpitInterface.reconcile_fields).first()

            # Rename ip_address to address and ensure mask for comparison consistency
            if 'ip_address' in incomming_obj:
                val = incomming_obj.pop('ip_address')
                incomming_obj['address'] = normalize_ip_address(val, Prefix) if val else None

            name = str(incomming_queryset.first().name)
            updated_time = incomming_queryset.first().last_updated
            title = name
            device = incomming_obj['device']
            incomming_obj['name'] = name

            incomming_change = {f: incomming_obj.get(f) for f in interface_fields if f in incomming_obj}
            
            # Remove vrf from comparison as it's ignored by default for interfaces
            if 'vrf' in incomming_change:
                incomming_change.pop('vrf')

            current_queryset = Interface.objects.filter(name=name, device=device)

            if current_queryset:
                netbox_interface = current_queryset.first()
                current_obj = current_queryset.values(*netbox_fields).first()
                current_obj['name'] = name
                
                # Fetch associated IP address from NetBox
                ip_ct = ContentType.objects.get_for_model(Interface)
                ip_obj = IPAddress.objects.filter(
                    assigned_object_type=ip_ct,
                    assigned_object_id=netbox_interface.pk
                ).first()
                
                if ip_obj:
                    current_obj['address'] = str(ip_obj.address)
                    current_obj['vrf'] = ip_obj.vrf.pk if ip_obj.vrf else None
                else:
                    current_obj['address'] = None
                    current_obj['vrf'] = None

                current_state = {f: current_obj.get(f) for f in interface_fields if f in current_obj}
                
                # Remove vrf from state as it's ignored for interfaces
                if 'vrf' in current_state:
                    current_state.pop('vrf')
                
                instance = netbox_interface
            else:
                current_state = None
                instance = None
                action = 'Created'
            

            if current_state and incomming_change:
                diff_added = shallow_compare_dict(
                    current_state or dict(),
                    incomming_change or dict(),
                    exclude=['last_updated', 'vrf'],
                )
                diff_removed = {
                    x: current_state.get(x) for x in diff_added
                } if current_state else {}
            else:
                diff_added = None
                diff_removed = None

            object_type = f'{Interface._meta.app_label} | {Interface._meta.verbose_name}'
        
        elif reconcile_type == 'prefix':
            self.queryset = models.SlurpitPrefix.objects.all()
            instance = self.get_object(pk=pk, **kwargs)
            diff_added = None
            diff_removed = None
            action = 'Updated'

            prefix_fields = ['prefix', *SlurpitPrefix.reconcile_fields]

            # Get ignore settings to determine if VRF should be ignored
            initial_obj = SlurpitPrefix.objects.filter(prefix=None).values(*SlurpitPrefix.ignore_fields).first()
            ignore_vrf = False
            if initial_obj and initial_obj.get('ignore_vrf'):
                ignore_vrf = True

            incomming_queryset = SlurpitPrefix.objects.filter(pk=pk)
            incomming_change = incomming_queryset.values(*prefix_fields).first()
            incomming_change['prefix'] = str(incomming_change['prefix'])

            # Remove VRF from change dict if ignore_vrf is set
            if ignore_vrf and 'vrf' in incomming_change:
                incomming_change.pop('vrf')

            prefix = str(incomming_queryset.first().prefix)
            updated_time = incomming_queryset.first().last_updated
            title = prefix
            vrf = None

            if incomming_change.get('vrf') is not None:
                vrf = VRF.objects.get(pk=incomming_change['vrf'])
            
            # Check if prefix already exists in NetBox
            # If ignore_vrf is set, search by prefix only (ignore VRF)
            if ignore_vrf:
                current_queryset = Prefix.objects.filter(prefix=prefix)
            else:
                current_queryset = Prefix.objects.filter(prefix=prefix, vrf=vrf)

            if current_queryset:
                current_obj = current_queryset.values(*prefix_fields).first()
                current_obj['prefix'] = str(current_obj['prefix'])
                current_state = {**current_obj}
                
                # Remove VRF from state dict if ignore_vrf is set
                if ignore_vrf and 'vrf' in current_state:
                    current_state.pop('vrf')

                instance = current_queryset.first()
            else:
                current_state = None
                instance = None
                action = 'Created'

            if current_state and incomming_change:
                # Exclude VRF from diff if ignore_vrf is set
                exclude_fields = ['last_updated']
                if ignore_vrf:
                    exclude_fields.append('vrf')

                diff_added = shallow_compare_dict(
                    current_state or dict(),
                    incomming_change or dict(),
                    exclude=exclude_fields,
                )
                diff_removed = {
                    x: current_state.get(x) for x in diff_added
                } if current_state else {}
            else:
                diff_added = None
                diff_removed = None


            object_type = f'{Prefix._meta.app_label} | {Prefix._meta.verbose_name}'

        else:
            instance = self.get_object(pk=pk, **kwargs)
            diff_added = None
            diff_removed = None
            action = 'Updated'

            ipam_fields = ['address', *SlurpitInitIPAddress.reconcile_fields]

            # Get ignore settings to determine if VRF should be ignored
            initial_obj = SlurpitInitIPAddress.objects.filter(address=None).values(*SlurpitInitIPAddress.ignore_fields).first()
            ignore_vrf = False
            if initial_obj and initial_obj.get('ignore_vrf'):
                ignore_vrf = True

            incomming_queryset = SlurpitInitIPAddress.objects.filter(pk=pk)
            incomming_obj = incomming_queryset.values(*ipam_fields).first()

            ipaddress = str(incomming_queryset.first().address)
            stripped_address = str(incomming_queryset.first().address.ip)
            updated_time = incomming_queryset.first().last_updated


            title = ipaddress
            vrf = incomming_obj.get('vrf')
            
            incomming_obj['address'] = ipaddress
            incomming_change = {**incomming_obj}
            
            # Remove VRF from change dict if ignore_vrf is set
            if ignore_vrf and 'vrf' in incomming_change:
                incomming_change.pop('vrf')

            # Check if IP address already exists in NetBox
            # If ignore_vrf is set, search by address only (ignore VRF)
            if ignore_vrf:
                current_queryset = IPAddress.objects.filter(address__net_host=stripped_address)
            else:
                current_queryset = IPAddress.objects.filter(address__net_host=stripped_address, vrf=vrf)
            if current_queryset:
                current_obj = current_queryset.values(*ipam_fields).first()
                # Use actual address from NetBox for comparison
                instance = current_queryset.first()
                current_obj['address'] = str(instance.address)
                current_state = {**current_obj}
                
                # Remove VRF from state dict if ignore_vrf is set
                if ignore_vrf and 'vrf' in current_state:
                    current_state.pop('vrf')
                
                instance = current_queryset.first()
            else:
                current_state = None
                instance = None
                action = 'Created'
            

            if current_state and incomming_change:
                # Exclude VRF from diff if ignore_vrf is set (already removed from dicts, but add to exclude for safety)
                exclude_fields = ['last_updated']
                if ignore_vrf:
                    exclude_fields.append('vrf')
                
                diff_added = shallow_compare_dict(
                    current_state or dict(),
                    incomming_change or dict(),
                    exclude=exclude_fields,
                )
                diff_removed = {
                    x: current_state.get(x) for x in diff_added
                } if current_state else {}
            else:
                diff_added = None
                diff_removed = None

            object_type = f'{IPAddress._meta.app_label} | {IPAddress._meta.verbose_name}'


        return render(
            request,
            self.template_name,
            {
                'object_action': instance.action,
                'title': title,
                'diff_added': diff_added,
                'diff_removed': diff_removed,
                'incomming_change': incomming_change,
                'current_state': current_state,
                'updated_time': updated_time,
                'action': action,
                'object_type': object_type
            },
        )

class SlurpitPrefixEditView(generic.ObjectEditView):
    """Edit view for a single Slurpit prefix."""
    queryset = SlurpitPrefix.objects.all()
    form = SlurpitPrefixForm
    template_name = 'slurpit_netbox/object_edit.html'

class SlurpitInterfaceEditView(generic.ObjectEditView):
    """Edit view for a single Slurpit interface."""
    queryset = SlurpitInterface.objects.all()
    form = forms.SlurpitDeviceInterfaceEditForm
    template_name = 'slurpit_netbox/object_edit.html'

class SlurpitIPAddressEditView(generic.ObjectEditView):
    """Edit view for a single Slurpit IP address."""
    queryset = SlurpitInitIPAddress.objects.all()
    form = forms.SlurpitInitIPAMEditForm
    template_name = 'slurpit_netbox/object_edit.html'

class SlurpitInterfaceBulkEditView(generic.BulkEditView):
    """Bulk edit view for Slurpit interfaces."""
    queryset = SlurpitInterface.objects.all()
    filterset = SlurpitInterfaceFilterSet
    table = tables.SlurpitInterfaceTable
    form = forms.SlurpitInterfaceBulkEditForm
    template_name = 'slurpit_netbox/object_bulkedit.html'

class SlurpitPrefixBulkEditView(generic.BulkEditView):
    """Bulk edit view for Slurpit prefixes."""
    queryset = SlurpitPrefix.objects.all()
    filterset = SlurpitPrefixFilterSet
    table = tables.SlurpitPrefixTable
    form = forms.SlurpitPrefixBulkEditForm
    template_name = 'slurpit_netbox/object_bulkedit.html'

class SlurpitIPAddressBulkEditView(generic.BulkEditView):
    """Bulk edit view for Slurpit IP addresses."""
    queryset = SlurpitInitIPAddress.objects.all()
    filterset = SlurpitIPAddressFilterSet
    table = tables.SlurpitIPAMTable
    form = forms.SlurpitIPAddressBulkEditForm
    template_name = 'slurpit_netbox/object_bulkedit.html'
