from django.apps import apps
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _

from circuits.choices import *
from dcim.models import CabledObjectModel
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
from netbox.models.features import (
    ContactsMixin,
    CustomFieldsMixin,
    CustomLinksMixin,
    ExportTemplatesMixin,
    ImageAttachmentsMixin,
    TagsMixin,
)
from netbox.models.mixins import DistanceMixin
from utilities.string import title

from .base import BaseCircuitType

__all__ = (
    'Circuit',
    'CircuitGroup',
    'CircuitGroupAssignment',
    'CircuitTermination',
    'CircuitType',
)


class CircuitType(BaseCircuitType):
    """
    Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
    "Long Haul," "Metro," or "Out-of-Band".
    """
    class Meta:
        ordering = ('name',)
        verbose_name = _('circuit type')
        verbose_name_plural = _('circuit types')


class Circuit(ContactsMixin, ImageAttachmentsMixin, DistanceMixin, PrimaryModel):
    """
    A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
    circuits. Each circuit is also assigned a CircuitType and a Site, and may optionally be assigned to a particular
    ProviderAccount. Circuit port speed and commit rate are measured in Kbps.
    """
    cid = models.CharField(
        max_length=100,
        verbose_name=_('circuit ID'),
        help_text=_('Unique circuit ID')
    )
    provider = models.ForeignKey(
        to='circuits.Provider',
        on_delete=models.PROTECT,
        related_name='circuits'
    )
    provider_account = models.ForeignKey(
        to='circuits.ProviderAccount',
        on_delete=models.PROTECT,
        related_name='circuits',
        blank=True,
        null=True
    )
    type = models.ForeignKey(
        to='circuits.CircuitType',
        on_delete=models.PROTECT,
        related_name='circuits'
    )
    status = models.CharField(
        verbose_name=_('status'),
        max_length=50,
        choices=CircuitStatusChoices,
        default=CircuitStatusChoices.STATUS_ACTIVE
    )
    tenant = models.ForeignKey(
        to='tenancy.Tenant',
        on_delete=models.PROTECT,
        related_name='circuits',
        blank=True,
        null=True
    )
    install_date = models.DateField(
        blank=True,
        null=True,
        verbose_name=_('installed')
    )
    termination_date = models.DateField(
        blank=True,
        null=True,
        verbose_name=_('terminates')
    )
    commit_rate = models.PositiveIntegerField(
        blank=True,
        null=True,
        verbose_name=_('commit rate (Kbps)'),
        help_text=_("Committed rate")
    )

    # Cache associated CircuitTerminations
    termination_a = models.ForeignKey(
        to='circuits.CircuitTermination',
        on_delete=models.SET_NULL,
        related_name='+',
        editable=False,
        blank=True,
        null=True
    )
    termination_z = models.ForeignKey(
        to='circuits.CircuitTermination',
        on_delete=models.SET_NULL,
        related_name='+',
        editable=False,
        blank=True,
        null=True
    )

    group_assignments = GenericRelation(
        to='circuits.CircuitGroupAssignment',
        content_type_field='member_type',
        object_id_field='member_id',
        related_query_name='circuit'
    )

    clone_fields = (
        'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate',
        'description',
    )
    prerequisite_models = (
        'circuits.CircuitType',
        'circuits.Provider',
    )

    class Meta:
        ordering = ['provider', 'provider_account', 'cid']
        constraints = (
            models.UniqueConstraint(
                fields=('provider', 'cid'),
                name='%(app_label)s_%(class)s_unique_provider_cid'
            ),
            models.UniqueConstraint(
                fields=('provider_account', 'cid'),
                name='%(app_label)s_%(class)s_unique_provideraccount_cid'
            ),
        )
        indexes = (
            models.Index(fields=('provider', 'provider_account', 'cid')),  # Default ordering
        )
        verbose_name = _('circuit')
        verbose_name_plural = _('circuits')

    def __str__(self):
        return self.cid

    def get_status_color(self):
        return CircuitStatusChoices.colors.get(self.status)

    def clean(self):
        super().clean()

        if self.provider_account and self.provider != self.provider_account.provider:
            raise ValidationError({'provider_account': "The assigned account must belong to the assigned provider."})


class CircuitGroup(OrganizationalModel):
    """
    An administrative grouping of Circuits.
    """
    tenant = models.ForeignKey(
        to='tenancy.Tenant',
        on_delete=models.PROTECT,
        related_name='circuit_groups',
        blank=True,
        null=True
    )

    class Meta:
        ordering = ('name',)
        verbose_name = _('circuit group')
        verbose_name_plural = _('circuit groups')

    def __str__(self):
        return self.name


class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
    """
    Assignment of a physical or virtual circuit to a CircuitGroup with an optional priority.
    """
    member_type = models.ForeignKey(
        to='contenttypes.ContentType',
        on_delete=models.PROTECT,
        related_name='+'
    )
    member_id = models.PositiveBigIntegerField(
        verbose_name=_('member ID')
    )
    member = GenericForeignKey(
        ct_field='member_type',
        fk_field='member_id'
    )
    group = models.ForeignKey(
        to='circuits.CircuitGroup',
        on_delete=models.CASCADE,
        related_name='assignments'
    )
    priority = models.CharField(
        verbose_name=_('priority'),
        max_length=50,
        choices=CircuitPriorityChoices,
        blank=True,
        null=True
    )
    prerequisite_models = (
        'circuits.CircuitGroup',
    )

    class Meta:
        ordering = ('group', 'member_type', 'member_id', 'priority', 'pk')
        constraints = (
            models.UniqueConstraint(
                fields=('member_type', 'member_id', 'group'),
                name='%(app_label)s_%(class)s_unique_member_group'
            ),
        )
        indexes = (
            models.Index(fields=('group', 'member_type', 'member_id', 'priority', 'id')),  # Default ordering
        )
        verbose_name = _('Circuit group assignment')
        verbose_name_plural = _('Circuit group assignments')

    def __str__(self):
        if self.priority:
            return f"{self.group} ({self.get_priority_display()})"
        return str(self.group)

    def get_absolute_url(self):
        return reverse('circuits:circuitgroupassignment', args=[self.pk])


class CircuitTermination(
    CustomFieldsMixin,
    CustomLinksMixin,
    ExportTemplatesMixin,
    TagsMixin,
    ChangeLoggedModel,
    CabledObjectModel
):
    circuit = models.ForeignKey(
        to='circuits.Circuit',
        on_delete=models.CASCADE,
        related_name='terminations'
    )
    term_side = models.CharField(
        max_length=1,
        choices=CircuitTerminationSideChoices,
        verbose_name=_('termination side')
    )
    termination_type = models.ForeignKey(
        to='contenttypes.ContentType',
        on_delete=models.PROTECT,
        related_name='+',
        blank=True,
        null=True
    )
    termination_id = models.PositiveBigIntegerField(
        blank=True,
        null=True
    )
    termination = GenericForeignKey(
        ct_field='termination_type',
        fk_field='termination_id'
    )
    port_speed = models.PositiveIntegerField(
        verbose_name=_('port speed (Kbps)'),
        blank=True,
        null=True,
        help_text=_('Physical circuit speed')
    )
    upstream_speed = models.PositiveIntegerField(
        blank=True,
        null=True,
        verbose_name=_('upstream speed (Kbps)'),
        help_text=_('Upstream speed, if different from port speed')
    )
    xconnect_id = models.CharField(
        max_length=50,
        blank=True,
        verbose_name=_('cross-connect ID'),
        help_text=_('ID of the local cross-connect')
    )
    pp_info = models.CharField(
        max_length=100,
        blank=True,
        verbose_name=_('patch panel/port(s)'),
        help_text=_('Patch panel ID and port number(s)')
    )
    description = models.CharField(
        verbose_name=_('description'),
        max_length=200,
        blank=True
    )

    # Cached associations to enable efficient filtering
    _provider_network = models.ForeignKey(
        to='circuits.ProviderNetwork',
        on_delete=models.PROTECT,
        related_name='circuit_terminations',
        blank=True,
        null=True
    )
    _location = models.ForeignKey(
        to='dcim.Location',
        on_delete=models.CASCADE,
        related_name='circuit_terminations',
        blank=True,
        null=True
    )
    _site = models.ForeignKey(
        to='dcim.Site',
        on_delete=models.CASCADE,
        related_name='circuit_terminations',
        blank=True,
        null=True
    )
    _region = models.ForeignKey(
        to='dcim.Region',
        on_delete=models.CASCADE,
        related_name='circuit_terminations',
        blank=True,
        null=True
    )
    _site_group = models.ForeignKey(
        to='dcim.SiteGroup',
        on_delete=models.CASCADE,
        related_name='circuit_terminations',
        blank=True,
        null=True
    )

    class Meta:
        ordering = ['circuit', 'term_side']
        constraints = (
            models.UniqueConstraint(
                fields=('circuit', 'term_side'),
                name='%(app_label)s_%(class)s_unique_circuit_term_side'
            ),
        )
        indexes = (
            models.Index(fields=('termination_type', 'termination_id')),
        )
        verbose_name = _('circuit termination')
        verbose_name_plural = _('circuit terminations')

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Cache original values to detect changes
        self._orig_circuit_id = self.__dict__.get('circuit_id')
        self._orig_term_side = self.__dict__.get('term_side')

    def __str__(self):
        return f'{self.circuit}: Termination {self.term_side}'

    def get_absolute_url(self):
        return reverse('circuits:circuittermination', args=[self.pk])

    def clean(self):
        if self.termination_type and not (self.termination or self.termination_id):
            termination_type = self.termination_type.model_class()
            raise ValidationError(
                _("Please select a {termination_type}.").format(
                    termination_type=_(title(termination_type._meta.verbose_name))
                )
            )
        super().clean()

        if self.termination is None:
            raise ValidationError(_("A circuit termination must attach to a terminating object."))

    def save(self, *args, **kwargs):
        is_new = self._state.adding
        update_fields = kwargs.get('update_fields')

        # Only consider circuit/term_side changes if those fields
        # are actually being persisted
        if update_fields is not None:
            tracking_relevant = 'circuit' in update_fields or 'term_side' in update_fields
        else:
            tracking_relevant = True

        circuit_changed = tracking_relevant and self._orig_circuit_id and self._orig_circuit_id != self.circuit_id
        term_side_changed = tracking_relevant and self._orig_term_side and self._orig_term_side != self.term_side

        # Cache objects associated with the terminating object (for filtering)
        self.cache_related_objects()

        super().save(*args, **kwargs)

        # Clear the old termination reference if circuit or term_side changed
        if circuit_changed or term_side_changed:
            old_termination_name = f'termination_{self._orig_term_side.lower()}'
            Circuit.objects.filter(pk=self._orig_circuit_id).update(**{old_termination_name: None})

        # Update the cache if this is a new termination or circuit/term_side changed
        if is_new or circuit_changed or term_side_changed:
            # Update the new circuit's termination reference
            termination_name = f'termination_{self.term_side.lower()}'
            Circuit.objects.filter(pk=self.circuit_id).update(**{termination_name: self.pk})

            # Update cached values for subsequent saves
            self._orig_circuit_id = self.circuit_id
            self._orig_term_side = self.term_side

    def cache_related_objects(self):
        self._provider_network = self._region = self._site_group = self._site = self._location = None
        if self.termination_type:
            termination_type = self.termination_type.model_class()
            if termination_type == apps.get_model('dcim', 'region'):
                self._region = self.termination
            elif termination_type == apps.get_model('dcim', 'sitegroup'):
                self._site_group = self.termination
            elif termination_type == apps.get_model('dcim', 'site'):
                self._region = self.termination.region
                self._site_group = self.termination.group
                self._site = self.termination
            elif termination_type == apps.get_model('dcim', 'location'):
                self._region = self.termination.site.region
                self._site_group = self.termination.site.group
                self._site = self.termination.site
                self._location = self.termination
            elif termination_type == apps.get_model('circuits', 'providernetwork'):
                self._provider_network = self.termination
    cache_related_objects.alters_data = True

    def to_objectchange(self, action):
        objectchange = super().to_objectchange(action)
        objectchange.related_object = self.circuit
        return objectchange

    @property
    def parent_object(self):
        return self.circuit

    def get_peer_termination(self):
        peer_side = 'Z' if self.term_side == 'A' else 'A'
        try:
            return CircuitTermination.objects.prefetch_related('termination').get(
                circuit=self.circuit,
                term_side=peer_side
            )
        except CircuitTermination.DoesNotExist:
            return None
