"""
Views for data mapping configuration between NetBox and Slurpit.

This module handles:
- Field mapping configuration (NetBox to Slurpit)
- Default value configuration for reconciliation (Slurpit to NetBox)
- Synchronization of device data from NetBox to Slurpit
- Testing and previewing mapped data

The mapping system allows users to:
- Map NetBox device fields to Slurpit fields
- Configure default values for IP addresses, prefixes, and interfaces
- Sync device data from NetBox back to Slurpit
"""

from django.views.generic import View
from ..models import (
    SlurpitImportedDevice, 
    SlurpitMapping, 
    SlurpitSetting, 
    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, redirect
from ..forms import SlurpitMappingForm, SlurpitDeviceForm, SlurpitDeviceStatusForm, SlurpitInitIPAMForm, SlurpitDeviceInterfaceForm, SlurpitPrefixForm
from ..management.choices import *
from django.contrib import messages
from dcim.models import Device
from django.forms.models import model_to_dict
import requests
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponse, JsonResponse
from django.utils.html import escape
from urllib.parse import urlencode
from utilities.forms import restrict_form_fields
from utilities.exceptions import AbortRequest, PermissionsViolation
from django.db import transaction

# Batch size for bulk operations
BATCH_SIZE = 128

def get_device_dict(instance):
    """Convert a Device instance to a dictionary for mapping.
    
    Converts a NetBox Device object to a dictionary format suitable for field mapping.
    Handles special fields (device_type, platform, IP addresses) and custom fields.
    
    Args:
        instance: Device instance to convert
    
    Returns:
        Dictionary containing device data with:
        - All standard device fields
        - String representations of device_type, platform, primary_ip4, primary_ip6
        - Custom fields prefixed with 'cf_' (e.g., 'cf_slurpit_hostname')
    """
    device_dict = model_to_dict(instance)
    # Convert related objects to strings
    device_dict['device_type'] = str(instance.device_type) if instance.device_type is not None else None
    device_dict['platform'] = str(instance.platform) if instance.platform is not None else None
    device_dict['primary_ip4'] = str(instance.primary_ip4) if instance.primary_ip4 is not None else None
    device_dict['primary_ip6'] = str(instance.primary_ip6) if instance.primary_ip6 is not None else None

    # Add custom fields with 'cf_' prefix for mapping
    for custom_field in device_dict['custom_field_data']:
        device_dict[f'cf_{custom_field}'] = device_dict['custom_field_data'][custom_field]

    return device_dict

def post_slurpit_device(row, device_name):
    """Post device data to Slurpit API for synchronization.
    
    Sends device data to the Slurpit server's device sync endpoint.
    Used for syncing NetBox device data back to Slurpit.
    
    Args:
        row: Dictionary containing device data mapped from NetBox fields
        device_name: Name of the device being synced
    
    Returns:
        Dictionary containing:
        - API response data (status, messages, etc.)
        - device_name: Name of the device
        - error: Error message if request failed
        None if settings are not configured
    """
    try:
        setting = SlurpitSetting.objects.get()
        uri_base = setting.server_url
        headers = {
            'Authorization': f'Bearer {setting.api_key}',
            'useragent': 'netbox/requests',
            'accept': 'application/json',
            'Content-Type': 'application/json',
        }

        uri_devices = f"{uri_base}/api/devices/sync"
        
        try:
            # Add flag to ignore plugin processing on Slurpit side
            row["ignore_plugin"] = str(1)
            r = requests.post(uri_devices, headers=headers, json=row, timeout=15, verify=False)
            r = r.json()
            r["device_name"] = device_name
            return r
        except Exception as e:
            return {"error": str(e), "device_name": device_name}

    except ObjectDoesNotExist:
        return {"error": "Need to set the setting parameter", "device_name": device_name}
    
    return None

@method_decorator(slurpit_plugin_registered, name='dispatch')
class DataMappingView(View):
    """View for configuring data mappings between NetBox and Slurpit.
    
    This view handles two main operations:
    1. NetBox to Slurpit: Field mapping configuration and device synchronization
    2. Slurpit to NetBox: Default value configuration for reconciliation
    
    Supports multiple tabs:
    - 'netbox_to_slurpit': Configure field mappings and sync devices
    - 'slurpit_to_netbox': Configure default values for IP addresses, prefixes, interfaces
    """
    template_name = "slurpit_netbox/data_mapping.html"
    app_label = "dcim"
    model_name = "device"

    def get(self, request):
        """Handle GET requests for data mapping configuration.
        
        Supports:
        - Displaying mapping forms
        - Syncing all devices from NetBox to Slurpit
        - Displaying default value configuration forms
        
        Args:
            request: HTTP request object with optional query parameters:
                - sync: If present, triggers sync of all devices
                - tab: 'netbox_to_slurpit' or 'slurpit_to_netbox'
                - subtab: 'ipam', 'prefix', or 'interface' (for slurpit_to_netbox)
        
        Returns:
            Rendered template with forms or redirect after sync
        """
        sync = request.GET.get('sync', None)
        tab = request.GET.get('tab', "slurpit_to_netbox")
        subtab = request.GET.get('subtab', None)
        
        # Sync operation: Sync all NetBox devices to Slurpit
        if sync is not None:
            # Get all NetBox devices and convert to dictionaries
            netbox_devices = Device.objects.all()
            devices_array = [get_device_dict(device) for device in netbox_devices]

            # Get all field mappings
            objs = SlurpitMapping.objects.all()
            
            # Process each device
            for device in devices_array:
                row = {}
                # Build row data using mappings
                for obj in objs:
                    # Extract field name from 'device|field_name' format
                    target_field = obj.target_field.split('|')[1]
                    row[obj.source_field] = str(device[target_field]) if device[target_field] is not None else None

                    # Remove CIDR notation from IP addresses and FQDNs
                    if obj.source_field == 'ipv4' or obj.source_field == 'fqdn':
                        if row[obj.source_field] is not None:
                            row[obj.source_field] = row[obj.source_field].split('/')[0]

                # Post device data to Slurpit
                res = post_slurpit_device(row, device["name"])

                if res is None:
                    return redirect(f'{request.path}?tab={tab}')
                
                # Check for errors in response
                if res['status'] != 200:
                    for error in res["messages"]:
                        messages.error(request, f'{escape(error)}: {escape(res["messages"][error])}')

                    return redirect(f'{request.path}?tab={tab}')
                
            messages.success(request, "Sync from Netbox to Slurpit is done successfully.")
            return redirect(f'{request.path}?tab={tab}')

        # Prepare forms for display
        form = []

        # Get all existing mappings
        mappings = SlurpitMapping.objects.all()

        # Get appliance type from settings
        appliance_type = ''
        try:
            setting = SlurpitSetting.objects.get()
            appliance_type = setting.appliance_type
        except ObjectDoesNotExist:
            pass

        # Create forms for each existing mapping
        for mapping in mappings:
            form.append({
                "choice": mapping,
                "form": SlurpitMappingForm(choice_name=mapping, initial={"target_field": mapping.target_field})
            })

        # Create forms for adding new mappings and device selection
        new_form = SlurpitMappingForm(doaction="add")
        device_form = SlurpitDeviceForm()
        device_status_form = SlurpitDeviceStatusForm()

        # Handle Slurpit to NetBox default value configuration
        if tab == "slurpit_to_netbox":
            # IP Address default values
            if subtab == None or subtab == 'ipam':
                # Get or create default IP address object (address=None indicates default)
                obj = SlurpitInitIPAddress.objects.filter(address=None).first()
                if obj is not None:
                    form = SlurpitInitIPAMForm(instance=obj)
                else:
                    form = SlurpitInitIPAMForm(data={'enable_reconcile':True})
            # Prefix default values
            elif subtab == 'prefix':
                # Get or create default prefix object (prefix=None indicates default)
                obj = SlurpitPrefix.objects.filter(prefix=None).first()
                if obj is not None:
                    form = SlurpitPrefixForm(instance=obj)
                else:
                    form = SlurpitPrefixForm(data={'enable_reconcile':True, 'status': 'active'})
            # Interface default values
            else:
                # Get or create default interface object (name='' indicates default)
                obj = SlurpitInterface.objects.filter(name='').first()

                if obj is not None:
                    form = SlurpitDeviceInterfaceForm(instance=obj)
                else:
                    form = SlurpitDeviceInterfaceForm(data={'type': 'other', 'enable_reconcile':True})

        return render(
            request,
            self.template_name,
            {
                "form": form,
                "new_form": new_form,
                "device_form": device_form,
                "device_status_form": device_status_form,
                "appliance_type": appliance_type,
            }
        )

    def post(self, request):
        """Handle POST requests for data mapping operations.
        
        Supports multiple operations:
        - Test/preview: Get mapped data for a specific device
        - Add mapping: Create a new field mapping
        - Delete mapping: Remove field mappings
        - Save mappings: Update existing mappings
        - Sync devices: Get list of devices to sync
        - Update defaults: Save default values for reconciliation
        
        Args:
            request: HTTP request object with POST data
        
        Returns:
            JSON response for test/sync operations, redirect for other operations
        """
        tab = request.GET.get('tab', "slurpit_to_netbox")
        mapping_type = ''

        # Handle NetBox to Slurpit operations
        if tab == "netbox_to_slurpit":
            tab = "netbox_to_slurpit"
            test = request.POST.get('test')
            device_id = request.POST.get('device_id')

            # Test/preview: Get mapped data for a device
            if device_id is not None:
                # Empty device_id returns empty response
                if device_id == "":
                    return JsonResponse({})

                # Get device and convert to dictionary
                device = Device.objects.get(id=int(device_id))
                device = get_device_dict(device)

                # Build mapped row using field mappings
                row = {}
                objs = SlurpitMapping.objects.all()
                for obj in objs:
                    # Extract field name from 'device|field_name' format
                    target_field = obj.target_field.split('|')[1]
                    row[obj.source_field] = str(device[target_field]) if device[target_field] is not None else None
                    
                    # Remove CIDR notation from IP addresses and FQDNs
                    if row[obj.source_field] is not None:
                        if obj.source_field == 'ipv4' or obj.source_field == 'fqdn':
                            row[obj.source_field] = row[obj.source_field].split('/')[0]
                    
                # If test flag is set, actually post to Slurpit API
                if test is not None:
                    res = post_slurpit_device(row, device["name"])

                    if res is None:
                        return JsonResponse({"error": "Server Internal Error."})

                    return JsonResponse(res)

                # Otherwise return preview of mapped data
                return JsonResponse(row)
            
            # Handle different actions
            action = request.POST.get("action")
            
            # Add new mapping
            if action is None:
                source_field = request.POST.get("source_field")
                target_field = request.POST.get("target_field")

                try:
                    obj = SlurpitMapping.objects.create(source_field=source_field, target_field=target_field)
                    messages.success(request, f'Added a mapping which {source_field} field converts to {target_field} field.')
                except Exception:
                    messages.error(request, f'Failed to add a mapping which {source_field} field converts to {target_field} field.')

                return redirect(f'{request.path}?tab={tab}')
            
            # Delete mappings
            elif action == "delete":
                source_field_keys = request.POST.getlist('pk')
                SlurpitMapping.objects.filter(source_field__in=source_field_keys).delete()
                return redirect(f'{request.path}?tab={tab}')
            
            # Save/update mappings
            elif action == "save":
                source_fields = request.POST.getlist('source_field')
                target_fields = request.POST.getlist('target_field')
                count = len(source_fields)
                offset = 0
                qs = []
                
                # Get or create mappings and update target fields
                for i in range(count):
                    mapping, created = SlurpitMapping.objects.get_or_create(
                        source_field=source_fields[i], 
                        defaults={'target_field': target_fields[i]}
                    )
                    if not created:
                        mapping.target_field = target_fields[i]
                    qs.append(mapping)

                # Batch update mappings
                while offset < count:
                    batch_qs = qs[offset:offset + BATCH_SIZE]
                    to_import = []
                    for mapping in batch_qs:
                        to_import.append(mapping)

                    SlurpitMapping.objects.bulk_update(to_import, fields={'target_field'})
                    offset += BATCH_SIZE

                return redirect(f'{request.path}?tab={tab}')
            
            # Sync: Get list of device IDs to sync
            elif action == "sync":
                device_status = request.POST.get('status')
                # Get all devices or filter by status
                if device_status == "":
                    netbox_devices = Device.objects.all().values("id", "status")
                else:
                    netbox_devices = Device.objects.filter(status=device_status).values("id")
                
                # Extract device IDs
                device_names = []
                if netbox_devices:
                    for device in netbox_devices:
                        device_names.append(device['id'])
                
                return JsonResponse({"device": device_names})

        # Handle Slurpit to NetBox default value updates
        elif tab == "slurpit_to_netbox":
            mapping_type = request.POST.get('mappingtype')
            
            # Update IP Address default values
            if mapping_type == 'ipam':
                # Get or create default IP address object
                obj = SlurpitInitIPAddress.objects.filter(address=None).first()
                if obj is None:
                    obj = SlurpitInitIPAddress()

                form = SlurpitInitIPAMForm(data=request.POST, instance=obj)
                restrict_form_fields(form, request.user)

                if form.is_valid():
                    try:
                        with transaction.atomic():
                            obj = form.save()
                            messages.success(request, "Updated the Slurpit IP Address Default values successfully.")
                    except (AbortRequest, PermissionsViolation) as e:
                        form.add_error(None, e.message)
                else:
                    messages.error(request, "Slurpit IP Address Form Validation Failed.")
            
            # Update Interface default values
            elif mapping_type == 'interface':
                # Get or create default interface object
                obj = SlurpitInterface.objects.filter(name='').first()
                if obj is None:
                    obj = SlurpitInterface()

                form = SlurpitDeviceInterfaceForm(data=request.POST, instance=obj)
                restrict_form_fields(form, request.user)

                if form.is_valid():
                    try:
                        with transaction.atomic():
                            obj = form.save()
                            messages.success(request, "Updated the Slurpit Interface Default values successfully.")
                    except (AbortRequest, PermissionsViolation) as e:
                        form.add_error(None, e.message)
                else:
                    messages.error(request, "Slurpit Interface Form Validation Failed.")

            # Update Prefix default values
            elif mapping_type == 'prefix':
                # Get or create default prefix object
                obj = SlurpitPrefix.objects.filter(prefix=None).first()
                if obj is None:
                    obj = SlurpitPrefix()

                form = SlurpitPrefixForm(data=request.POST, instance=obj)
                restrict_form_fields(form, request.user)

                if form.is_valid():
                    try:
                        with transaction.atomic():
                            obj = form.save()
                            messages.success(request, "Updated the Slurpit Prefix Default values successfully.")
                    except (AbortRequest, PermissionsViolation) as e:
                        form.add_error(None, e.message)
                else:
                    messages.error(request, "Slurpit Prefix Form Validation Failed.")
        
        # Build redirect URL with appropriate query parameters
        base_url = request.path

        if mapping_type == "":
            query_string = urlencode({'tab': tab})
        else:
            query_string = urlencode({'tab': tab, 'subtab': mapping_type})
        url = f'{base_url}?{query_string}'
        return redirect(url)