import logging
from ipaddress import ip_address as ip_addr_obj, ip_network

from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.shortcuts import redirect, render
from django.views.generic import View

from ipam.models import IPAddress, Prefix, VLAN

# NetBox 4.3.x
from virtualization.models import VMInterface, VirtualDisk, VirtualMachine

from .utils import (
    IPAddressInfo,
    PrefixInfo,
    VLANInfo,
    VMDiskInfo,
    VMInterfaceInfo,
    VirtualMachineInfo,
)

logger = logging.getLogger(__name__)


def get_custom_field_value(obj, field_name):
    """Safely retrieve custom field from NetBox ORM object."""
    if hasattr(obj, "custom_field_data"):
        return obj.custom_field_data.get(field_name)
    if hasattr(obj, "cf") and isinstance(obj.cf, dict):
        return obj.cf.get(field_name)
    try:
        return obj.custom_fields.get(field_name)
    except Exception:
        pass
    return None


def set_custom_field_value(obj, field_name, value):
    """Safely set a custom field on a NetBox ORM object (supports cf/custom_field_data)."""
    if hasattr(obj, "custom_field_data") and isinstance(obj.custom_field_data, dict):
        obj.custom_field_data[field_name] = value
        return True
    if hasattr(obj, "cf") and isinstance(obj.cf, dict):
        obj.cf[field_name] = value
        return True
    return False


def safe_to_string(value):
    """Safely convert value to string, handling None."""
    if value is None:
        return ""
    if isinstance(value, list):
        return ", ".join([str(p.name) if hasattr(p, "name") else str(p) for p in value])
    return str(value)


def get_ips_in_prefix(prefix):
    """Fetch all IP addresses within a prefix."""
    try:
        prefix_net = ip_network(prefix.prefix, strict=False)
    except ValueError as e:
        logger.error(f"Invalid prefix: {e}")
        raise ValueError(f"Invalid prefix format: {e}")

    ips_in_prefix = []

    try:
        if hasattr(prefix, "ip_addresses"):
            query_ips = prefix.ip_addresses.all()
        else:
            query_ips = IPAddress.objects.filter(address__family=prefix.prefix.version)
    except Exception as e:
        logger.warning(f"Could not optimize IP query: {e}, falling back to all IPs")
        query_ips = IPAddress.objects.all()

    for ip in query_ips:
        try:
            ip_addr = ip_addr_obj(str(ip.address).split("/")[0])
            if ip_addr in prefix_net:
                ip_info = IPAddressInfo(
                    id=ip.id,
                    address=str(ip.address),
                    tenant_id=ip.tenant.id if ip.tenant else None,
                    tenant_name=ip.tenant.name if ip.tenant else "",
                    tenant_permissions=safe_to_string(get_custom_field_value(ip, "tenant_permissions")),
                    tenant_permissions_ro=safe_to_string(get_custom_field_value(ip, "tenant_permissions_ro")),
                )
                ips_in_prefix.append(ip_info)
        except (ValueError, AttributeError):
            continue

    return ips_in_prefix, prefix_net


class PrefixPermissionsSyncView(LoginRequiredMixin, PermissionRequiredMixin, View):
    """Prefix -> IPs (+ VLAN). Sync tenant + CFs."""

    permission_required = (
        "ipam.view_ipaddress",
        "ipam.change_ipaddress",
        "ipam.view_prefix",
        "ipam.view_vlan",
        "ipam.change_vlan",
    )

    def get(self, request, prefix_id):
        try:
            prefix = Prefix.objects.get(id=prefix_id)
        except Prefix.DoesNotExist:
            messages.error(request, f"Prefix with ID {prefix_id} not found")
            return redirect("ipam:prefix_list")

        if prefix.status == "container":
            messages.error(request, "Cannot synchronize permissions for container prefixes")
            return redirect("ipam:prefix_list")

        try:
            prefix_info = PrefixInfo(
                id=prefix.id,
                prefix=str(prefix.prefix),
                tenant_id=prefix.tenant.id if prefix.tenant else None,
                tenant_name=prefix.tenant.name if prefix.tenant else "",
                tenant_permissions=safe_to_string(get_custom_field_value(prefix, "tenant_permissions")),
                tenant_permissions_ro=safe_to_string(get_custom_field_value(prefix, "tenant_permissions_ro")),
            )

            vlan_info = None
            if getattr(prefix, "vlan", None):
                vlan = prefix.vlan
                vlan_info = VLANInfo(
                    id=vlan.id,
                    vid=getattr(vlan, "vid", None),
                    name=getattr(vlan, "name", "") or str(vlan),
                    tenant_id=vlan.tenant.id if getattr(vlan, "tenant", None) else None,
                    tenant_name=vlan.tenant.name if getattr(vlan, "tenant", None) else "",
                    tenant_permissions=safe_to_string(get_custom_field_value(vlan, "tenant_permissions")),
                    tenant_permissions_ro=safe_to_string(get_custom_field_value(vlan, "tenant_permissions_ro")),
                )

            ips_in_prefix, _ = get_ips_in_prefix(prefix)

            ips_to_sync = []
            ips_synced = []
            for ip_info in ips_in_prefix:
                if (
                    ip_info.tenant_permissions != prefix_info.tenant_permissions
                    or ip_info.tenant_permissions_ro != prefix_info.tenant_permissions_ro
                    or ip_info.tenant_id != prefix_info.tenant_id
                ):
                    ips_to_sync.append(ip_info)
                else:
                    ips_synced.append(ip_info)

            vlan_needs_sync = False
            if vlan_info is not None:
                vlan_needs_sync = not (
                    vlan_info.tenant_id == prefix_info.tenant_id
                    and vlan_info.tenant_permissions == prefix_info.tenant_permissions
                    and vlan_info.tenant_permissions_ro == prefix_info.tenant_permissions_ro
                )

            return render(
                request,
                "netbox_custom_permissions_synchronization/ip_permissions_sync.html",
                {
                    "prefix": prefix_info,
                    "vlan": vlan_info,
                    "vlan_needs_sync": vlan_needs_sync,
                    "ips_to_sync": ips_to_sync,
                    "ips_synced": ips_synced,
                    "ips_total": len(ips_in_prefix),
                },
            )

        except Exception as e:
            logger.error(f"Error in Prefix GET: {str(e)}", exc_info=True)
            messages.error(request, f"An error occurred: {str(e)}")
            return redirect("ipam:prefix_list")

    def post(self, request, prefix_id):
        try:
            prefix = Prefix.objects.get(id=prefix_id)
        except Prefix.DoesNotExist:
            messages.error(request, "Prefix not found")
            return redirect("ipam:prefix_list")

        if prefix.status == "container":
            messages.error(request, "Cannot synchronize permissions for container prefixes")
            return redirect("ipam:prefix_list")

        try:
            prefix_tenant = prefix.tenant
            prefix_permissions = get_custom_field_value(prefix, "tenant_permissions")
            prefix_permissions_ro = get_custom_field_value(prefix, "tenant_permissions_ro")

            # VLAN
            vlan_updated = False
            vlan_failed = False
            if getattr(prefix, "vlan", None):
                try:
                    vlan = VLAN.objects.get(id=prefix.vlan.id)
                    vlan_changed = False

                    if getattr(vlan, "tenant_id", None) != (prefix_tenant.id if prefix_tenant else None):
                        vlan.tenant = prefix_tenant
                        vlan_changed = True

                    if get_custom_field_value(vlan, "tenant_permissions") != prefix_permissions:
                        set_custom_field_value(vlan, "tenant_permissions", prefix_permissions or [])
                        vlan_changed = True

                    if get_custom_field_value(vlan, "tenant_permissions_ro") != prefix_permissions_ro:
                        set_custom_field_value(vlan, "tenant_permissions_ro", prefix_permissions_ro or [])
                        vlan_changed = True

                    if vlan_changed:
                        vlan.save()
                        vlan_updated = True

                except Exception:
                    vlan_failed = True
                    logger.error(
                        f"Error updating VLAN for prefix {prefix.id} (vlan={getattr(prefix.vlan, 'id', None)}):",
                        exc_info=True,
                    )

            # IPs
            ips_in_prefix, _ = get_ips_in_prefix(prefix)

            updated_count = 0
            failed_count = 0

            for ip_info in ips_in_prefix:
                try:
                    ip = IPAddress.objects.get(id=ip_info.id)
                    changed = False

                    if ip.tenant_id != (prefix_tenant.id if prefix_tenant else None):
                        ip.tenant = prefix_tenant
                        changed = True

                    if get_custom_field_value(ip, "tenant_permissions") != prefix_permissions:
                        set_custom_field_value(ip, "tenant_permissions", prefix_permissions or [])
                        changed = True

                    if get_custom_field_value(ip, "tenant_permissions_ro") != prefix_permissions_ro:
                        set_custom_field_value(ip, "tenant_permissions_ro", prefix_permissions_ro or [])
                        changed = True

                    if changed:
                        ip.save()
                        updated_count += 1

                except Exception as e:
                    failed_count += 1
                    logger.error(f"Error updating IP {ip_info.id}: {str(e)}", exc_info=True)

            parts = []
            if updated_count:
                parts.append(f"synchronized {updated_count} IP address(es)")
            if vlan_updated:
                parts.append("synchronized VLAN")
            if failed_count:
                parts.append(f"failed to update {failed_count} IP address(es)")
            if vlan_failed:
                parts.append("failed to update VLAN")

            if parts:
                if vlan_failed and updated_count == 0 and failed_count == 0 and not vlan_updated:
                    messages.error(request, "Failed to update VLAN")
                else:
                    messages.success(request, f"Successfully {' and '.join(parts)}")
            else:
                messages.info(request, "No changes needed")

            return redirect(request.path)

        except Exception as e:
            logger.error(f"Error in Prefix POST: {str(e)}", exc_info=True)
            messages.error(request, f"An error occurred: {str(e)}")
            return redirect(request.path)


class VMPermissionsSyncView(LoginRequiredMixin, PermissionRequiredMixin, View):
    """
    Virtual Machine -> VM Interfaces + Virtual Disks.

    NOTE: VMInterface and VirtualDisk do NOT have a tenant field in NetBox 4.3.x,
    so we only sync custom fields.
    """

    permission_required = (
        "virtualization.view_virtualmachine",
        "virtualization.view_vminterface",
        "virtualization.change_vminterface",
        "virtualization.view_virtualdisk",
        "virtualization.change_virtualdisk",
    )

    def get(self, request, vm_id):
        try:
            vm = VirtualMachine.objects.get(id=vm_id)
        except VirtualMachine.DoesNotExist:
            messages.error(request, f"Virtual Machine with ID {vm_id} not found")
            return redirect("virtualization:virtualmachine_list")

        try:
            vm_info = VirtualMachineInfo(
                id=vm.id,
                name=str(vm.name),
                tenant_id=vm.tenant.id if vm.tenant else None,
                tenant_name=vm.tenant.name if vm.tenant else "",
                tenant_permissions=safe_to_string(get_custom_field_value(vm, "tenant_permissions")),
                tenant_permissions_ro=safe_to_string(get_custom_field_value(vm, "tenant_permissions_ro")),
            )

            interfaces = VMInterface.objects.filter(virtual_machine=vm).order_by("name")
            disks = VirtualDisk.objects.filter(virtual_machine=vm).order_by("name")

            interfaces_to_sync = []
            interfaces_synced = []
            for iface in interfaces:
                info = VMInterfaceInfo(
                    id=iface.id,
                    name=str(iface.name),
                    tenant_id=None,
                    tenant_name="",
                    tenant_permissions=safe_to_string(get_custom_field_value(iface, "tenant_permissions")),
                    tenant_permissions_ro=safe_to_string(get_custom_field_value(iface, "tenant_permissions_ro")),
                )
                if (
                    info.tenant_permissions != vm_info.tenant_permissions
                    or info.tenant_permissions_ro != vm_info.tenant_permissions_ro
                ):
                    interfaces_to_sync.append(info)
                else:
                    interfaces_synced.append(info)

            disks_to_sync = []
            disks_synced = []
            for d in disks:
                info = VMDiskInfo(
                    id=d.id,
                    name=str(d.name),
                    tenant_id=None,
                    tenant_name="",
                    tenant_permissions=safe_to_string(get_custom_field_value(d, "tenant_permissions")),
                    tenant_permissions_ro=safe_to_string(get_custom_field_value(d, "tenant_permissions_ro")),
                )
                if (
                    info.tenant_permissions != vm_info.tenant_permissions
                    or info.tenant_permissions_ro != vm_info.tenant_permissions_ro
                ):
                    disks_to_sync.append(info)
                else:
                    disks_synced.append(info)

            return render(
                request,
                "netbox_custom_permissions_synchronization/vm_permissions_sync.html",
                {
                    "vm": vm_info,
                    "interfaces_to_sync": interfaces_to_sync,
                    "interfaces_synced": interfaces_synced,
                    "disks_to_sync": disks_to_sync,
                    "disks_synced": disks_synced,
                    "interfaces_total": len(interfaces),
                    "disks_total": len(disks),
                },
            )

        except Exception as e:
            logger.error(f"Error in VM GET: {str(e)}", exc_info=True)
            messages.error(request, f"An error occurred: {str(e)}")
            return redirect("virtualization:virtualmachine_list")

    def post(self, request, vm_id):
        try:
            vm = VirtualMachine.objects.get(id=vm_id)
        except VirtualMachine.DoesNotExist:
            messages.error(request, "Virtual Machine not found")
            return redirect("virtualization:virtualmachine_list")

        try:
            vm_permissions = get_custom_field_value(vm, "tenant_permissions")
            vm_permissions_ro = get_custom_field_value(vm, "tenant_permissions_ro")

            updated_ifaces = 0
            failed_ifaces = 0
            updated_disks = 0
            failed_disks = 0

            # Interfaces: CF-only
            for iface in VMInterface.objects.filter(virtual_machine=vm):
                try:
                    changed = False

                    if get_custom_field_value(iface, "tenant_permissions") != vm_permissions:
                        set_custom_field_value(iface, "tenant_permissions", vm_permissions or [])
                        changed = True

                    if get_custom_field_value(iface, "tenant_permissions_ro") != vm_permissions_ro:
                        set_custom_field_value(iface, "tenant_permissions_ro", vm_permissions_ro or [])
                        changed = True

                    if changed:
                        iface.save()
                        updated_ifaces += 1

                except Exception as e:
                    failed_ifaces += 1
                    logger.error(f"Error updating VM interface {iface.id}: {str(e)}", exc_info=True)

            # Disks: CF-only
            for disk in VirtualDisk.objects.filter(virtual_machine=vm):
                try:
                    changed = False

                    if get_custom_field_value(disk, "tenant_permissions") != vm_permissions:
                        set_custom_field_value(disk, "tenant_permissions", vm_permissions or [])
                        changed = True

                    if get_custom_field_value(disk, "tenant_permissions_ro") != vm_permissions_ro:
                        set_custom_field_value(disk, "tenant_permissions_ro", vm_permissions_ro or [])
                        changed = True

                    if changed:
                        disk.save()
                        updated_disks += 1

                except Exception as e:
                    failed_disks += 1
                    logger.error(f"Error updating VM disk {disk.id}: {str(e)}", exc_info=True)

            parts = []
            if updated_ifaces:
                parts.append(f"synchronized {updated_ifaces} interface(s)")
            if updated_disks:
                parts.append(f"synchronized {updated_disks} disk(s)")
            if failed_ifaces:
                parts.append(f"failed {failed_ifaces} interface(s)")
            if failed_disks:
                parts.append(f"failed {failed_disks} disk(s)")

            if parts:
                messages.success(request, f"Successfully {' and '.join(parts)}")
            else:
                messages.info(request, "No changes needed")

            return redirect(request.path)

        except Exception as e:
            logger.error(f"Error in VM POST: {str(e)}", exc_info=True)
            messages.error(request, f"An error occurred: {str(e)}")
            return redirect(request.path)
