from pathlib import Path
from typing import Optional

from circuits.models import Circuit
from dcim.models import Device, DeviceRole, PowerPanel, PowerFeed
from extras.models import Tag
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.templatetags.static import static
from django.urls import reverse
from netbox.models import NetBoxModel
from netbox.models.features import (
    ChangeLoggingMixin,
    ExportTemplatesMixin,
    EventRulesMixin,
)

from netbox_topology_views.utils import (
    CONF_IMAGE_DIR,
    IMAGE_DIR,
    Role,
    find_image_url,
    get_model_role,
    image_static_url,
)

from netbox_topology_views.choices import NodeLabelItems

class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, EventRulesMixin):
    class Meta:
        indexes = [
            models.Index(fields=["content_type", "object_id"]),
        ]

    objects: "models.Manager[RoleImage]"

    image = models.CharField("Path within the netbox static directory", max_length=255)

    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField(null=True, blank=True)

    __role: Optional[Role] = None

    _netbox_private = True

    @property
    def role(self) -> Role:
        if self.__role:
            return self.__role

        model_class = self.content_type.model_class()

        if not model_class:
            raise ValueError(f"Invalid content type: {self.content_type}")

        if model_class == DeviceRole:
            device_role: DeviceRole = DeviceRole.objects.get(pk=self.object_id)
            self.__role = Role(slug=device_role.slug, name=device_role.name)
            return self.__role

        self.__role = get_model_role(model_class)
        return self.__role

    def __str__(self):
        return f"{self.role} - {self.image}"

    def get_image(self) -> Path:
        """Get Icon

        returns the model's image's absolute path in the filesystem
        raises ValueError if the file cannot be found
        """
        path = Path(settings.STATIC_ROOT) / self.image

        if not path.exists():
            raise ValueError(f"{self.role} path '{path}' does not exists")

        return path

    def get_default_image(self, dir: Path = CONF_IMAGE_DIR):
        """Get default image

        will attempt to find image in given directory with any file extension,
        otherwise will try to find a `role-unknown` image

        fallback is `STATIC_ROOT/netbox_topology_views/img/role-unknown.png`
        """
        if url := find_image_url(self.role.slug, dir):
            return url

        # fallback to default role unknown image
        return image_static_url(IMAGE_DIR / "role-unknown.png")

    def get_image_url(self, dir: Path = CONF_IMAGE_DIR) -> str:
        try:
            self.get_image()
        except ValueError:
            return self.get_default_image(dir)
        return static(f"/{self.image}")

class CoordinateGroup(NetBoxModel):
    """
    A coordinate group is used to display the topology for a particular group. 
    This allows different visualizations with the same devices.
    """
    name = models.CharField(
        max_length=100,
        unique=True,
    )

    description = models.CharField(
        max_length=255,
        blank = True,
    )

    _netbox_private = True

    class Meta:
        ordering = ['name']

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('plugins:netbox_topology_views:coordinategroup', args=[self.pk])

class Coordinate(NetBoxModel):
    """
    Coordinates are being used to place devices in a topology view onto a certain 
    position. Devices belong to one or more coordinate groups. They have to 
    be unique together.
    """
    device = models.ForeignKey(Device, on_delete=models.CASCADE)
    group = models.ForeignKey(CoordinateGroup, on_delete=models.CASCADE)
    
    x = models.IntegerField(
        help_text='X-coordinate of the device (horizontal) on the canvas. '
            'Smaller values correspond to a position further to the left on the monitor.',
    )
    y = models.IntegerField(
        help_text='Y-coordinate of the device (vertical) on the canvas. '
            'Smaller values correspond to a position further up on the monitor.',
    )

    _netbox_private = True

    def get_or_create_default_group(group_id):
        # Default group named "default" must always exist in order to make sure
        # that coordinate values can be stored even if no coordinate group has been
        # selected. The default group will be added automatically if it does not exist.
        try:
            if CoordinateGroup.objects.filter(name="default"):
                group = CoordinateGroup.objects.get(name="default")
                group_id = group.pk
            else:
                group = CoordinateGroup(
                    name="default", 
                    description="Automatically generated default group. If you delete "
                        "this group, all default coordinates are gone for good but "
                        "the group itself will be re-created."
                )
                group.save()
                group_id = group.pk
        except:
            return False
        return group_id

    class Meta:
        ordering = ['group', 'device']
        unique_together = ('device', 'group')

    def __str__(self):
        return f'{self.x};{self.y}'

    def get_absolute_url(self):
        return reverse('plugins:netbox_topology_views:coordinate', args=[self.pk])

class CircuitCoordinate(NetBoxModel):
    """
    Coordinates are being used to place devices in a topology view onto a certain 
    position. Devices belong to one or more coordinate groups. They have to 
    be unique together.
    """
    device = models.ForeignKey(Circuit, on_delete=models.CASCADE)
    group = models.ForeignKey(CoordinateGroup, on_delete=models.CASCADE)
    
    x = models.IntegerField(
        help_text='X-coordinate of the device (horizontal) on the canvas. '
            'Smaller values correspond to a position further to the left on the monitor.',
    )
    y = models.IntegerField(
        help_text='Y-coordinate of the device (vertical) on the canvas. '
            'Smaller values correspond to a position further up on the monitor.',
    )

    _netbox_private = True

    def get_or_create_default_group(group_id):
        # Default group named "default" must always exist in order to make sure
        # that coordinate values can be stored even if no coordinate group has been
        # selected. The default group will be added automatically if it does not exist.
        try:
            if CoordinateGroup.objects.filter(name="default"):
                group = CoordinateGroup.objects.get(name="default")
                group_id = group.pk
            else:
                group = CoordinateGroup(
                    name="default", 
                    description="Automatically generated default group. If you delete "
                        "this group, all default coordinates are gone for good but "
                        "the group itself will be re-created."
                )
                group.save()
                group_id = group.pk
        except:
            return False
        return group_id

    class Meta:
        ordering = ['group', 'device']
        unique_together = ('device', 'group')

    def __str__(self):
        return f'{self.x};{self.y}'

    def get_absolute_url(self):
        return reverse('plugins:netbox_topology_views:circuitcoordinate', args=[self.pk])

class PowerPanelCoordinate(NetBoxModel):
    """
    Coordinates are being used to place devices in a topology view onto a certain 
    position. Devices belong to one or more coordinate groups. They have to 
    be unique together.
    """
    device = models.ForeignKey(PowerPanel, on_delete=models.CASCADE)
    group = models.ForeignKey(CoordinateGroup, on_delete=models.CASCADE)
    
    x = models.IntegerField(
        help_text='X-coordinate of the device (horizontal) on the canvas. '
            'Smaller values correspond to a position further to the left on the monitor.',
    )
    y = models.IntegerField(
        help_text='Y-coordinate of the device (vertical) on the canvas. '
            'Smaller values correspond to a position further up on the monitor.',
    )

    _netbox_private = True

    def get_or_create_default_group(group_id):
        # Default group named "default" must always exist in order to make sure
        # that coordinate values can be stored even if no coordinate group has been
        # selected. The default group will be added automatically if it does not exist.
        try:
            if CoordinateGroup.objects.filter(name="default"):
                group = CoordinateGroup.objects.get(name="default")
                group_id = group.pk
            else:
                group = CoordinateGroup(
                    name="default", 
                    description="Automatically generated default group. If you delete "
                        "this group, all default coordinates are gone for good but "
                        "the group itself will be re-created."
                )
                group.save()
                group_id = group.pk
        except:
            return False
        return group_id

    class Meta:
        ordering = ['group', 'device']
        unique_together = ('device', 'group')

    def __str__(self):
        return f'{self.x};{self.y}'

    def get_absolute_url(self):
        return reverse('plugins:netbox_topology_views:powerpanelcoordinate', args=[self.pk])

class PowerFeedCoordinate(NetBoxModel):
    """
    Coordinates are being used to place devices in a topology view onto a certain 
    position. Devices belong to one or more coordinate groups. They have to 
    be unique together.
    """
    device = models.ForeignKey(PowerFeed, on_delete=models.CASCADE)
    group = models.ForeignKey(CoordinateGroup, on_delete=models.CASCADE)
    
    x = models.IntegerField(
        help_text='X-coordinate of the device (horizontal) on the canvas. '
            'Smaller values correspond to a position further to the left on the monitor.',
    )
    y = models.IntegerField(
        help_text='Y-coordinate of the device (vertical) on the canvas. '
            'Smaller values correspond to a position further up on the monitor.',
    )

    _netbox_private = True

    def get_or_create_default_group(group_id):
        # Default group named "default" must always exist in order to make sure
        # that coordinate values can be stored even if no coordinate group has been
        # selected. The default group will be added automatically if it does not exist.
        try:
            if CoordinateGroup.objects.filter(name="default"):
                group = CoordinateGroup.objects.get(name="default")
                group_id = group.pk
            else:
                group = CoordinateGroup(
                    name="default", 
                    description="Automatically generated default group. If you delete "
                        "this group, all default coordinates are gone for good but "
                        "the group itself will be re-created."
                )
                group.save()
                group_id = group.pk
        except:
            return False
        return group_id

    class Meta:
        ordering = ['group', 'device']
        unique_together = ('device', 'group')

    def __str__(self):
        return f'{self.x};{self.y}'

    def get_absolute_url(self):
        return reverse('plugins:netbox_topology_views:powerfeedcoordinate', args=[self.pk])

class IndividualOptions(NetBoxModel):
    CHOICES = (
        ('interface', 'interface'),
        ('frontport', 'frontport'),
        ('rearport', 'rearport'),
        ('poweroutlet', 'poweroutlet'),
        ('powerport', 'powerport'),
        ('consoleport', 'consoleport'),
        ('consoleserverport', 'consoleserverport'),
    )

    user_id = models.IntegerField(
        null=True,
        unique=True
    )
    ignore_cable_type = models.CharField(
        max_length = 255,
        blank = True,
    )
    preselected_device_roles = models.ManyToManyField(
        to='dcim.DeviceRole',
        related_name='+',
        blank=True,
        db_table='netbox_topology_views_individualoptions_preselected_device',
    )
    preselected_tags = models.ManyToManyField(
        to='extras.Tag',
        related_name='+',
        blank=True,
        db_table='netbox_topology_views_individualoptions_preselected_tag',
    )
    save_coords = models.BooleanField(
        default=False
    )
    show_unconnected = models.BooleanField(
        default=False
    )
    show_cables = models.BooleanField(
        default=False
    )
    show_logical_connections = models.BooleanField(
        default=False
    )
    show_single_cable_logical_conns = models.BooleanField(
        default=False
    )
    show_neighbors = models.BooleanField(
        default=False
    )
    show_circuit = models.BooleanField(
        default=False
    )
    show_power = models.BooleanField(
        default=False
    )
    show_wireless = models.BooleanField(
        default=False
    )
    group_sites = models.BooleanField(
        default=False
    )
    group_locations = models.BooleanField(
        default=False
    )
    group_racks = models.BooleanField(
        default=False
    )
    group_virtualchassis = models.BooleanField(
        default=False
    )
    draw_default_layout = models.BooleanField(
        default=False
    )
    straight_cables = models.BooleanField(
        default=False
    )
    draw_termination_labels = models.BooleanField(
        default=False
    )
    grid_size = models.PositiveSmallIntegerField(
        default=0
    )
    node_label_items = models.CharField(
        max_length=255,
        blank=True
    )
    
    _netbox_private = True

    def __str___(self):
        return f"{self.user_id}"
