import re

from django.contrib.contenttypes.fields import ContentType
from django.contrib.postgres.fields import JSONField
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from mptt.models import MPTTModel, TreeForeignKey
from slugify import slugify

from establishment.models import Establishment
from location.models import Country, Region, WineRegion, WineSubRegion, City
from product.models import Product
from review.models import Review
from collection import tasks
from translation.models import Language
from utils.methods import transform_into_readable_str
from utils.models import (
    ProjectBaseMixin, TJSONField, TranslatedFieldsMixin,
    URLImageMixin, IntermediateGalleryModelMixin
)
from utils.querysets import RelatedObjectsCountMixin


# Mixins
class CollectionNameMixin(models.Model):
    """CollectionName mixin"""
    name = models.CharField(_('name'), max_length=250)

    class Meta:
        """Meta class"""
        abstract = True


class CollectionDateMixin(models.Model):
    """CollectionDate mixin"""
    start = models.DateTimeField(blank=True, null=True, default=None,
                                 verbose_name=_('start'))
    end = models.DateTimeField(blank=True, null=True, default=None,
                               verbose_name=_('end'))

    class Meta:
        """Meta class"""
        abstract = True


# Models
class CollectionQuerySet(RelatedObjectsCountMixin):
    """QuerySet for model Collection"""

    def by_country_code(self, code):
        """Filter collection by country code."""
        return self.filter(country__code=code)

    def published(self):
        """Returned only published collection"""
        return self.filter(is_publish=True)


class Collection(ProjectBaseMixin, CollectionDateMixin,
                 TranslatedFieldsMixin, URLImageMixin):
    """Collection model."""
    STR_FIELD_NAME = 'name'

    ORDINARY = 0  # Ordinary collection
    POP = 1  # POP collection

    COLLECTION_TYPES = (
        (ORDINARY, _('Ordinary')),
        (POP, _('Pop')),
    )

    name = TJSONField(verbose_name=_('name'),
                      help_text='{"en-GB":"some text"}')
    collection_type = models.PositiveSmallIntegerField(choices=COLLECTION_TYPES,
                                                       default=ORDINARY,
                                                       verbose_name=_('Collection type'))
    is_publish = models.BooleanField(
        default=False, verbose_name=_('Publish status'))
    on_top = models.BooleanField(
        default=False, verbose_name=_('Position on top'))
    country = models.ForeignKey(
        'location.Country', verbose_name=_('country'), on_delete=models.CASCADE)
    block_size = JSONField(
        _('collection block properties'), null=True, blank=True,
        default=None, help_text='{"width": "250px", "height":"250px"}')
    description = TJSONField(
        _('description'), null=True, blank=True,
        default=None, help_text='{"en-GB":"some text"}')
    slug = models.SlugField(max_length=255, unique=True,
                            verbose_name=_('Collection slug'), editable=True, null=True)
    old_id = models.IntegerField(null=True, blank=True)

    rank = models.IntegerField(null=True, default=None)

    objects = CollectionQuerySet.as_manager()

    class Meta:
        """Meta class."""
        verbose_name = _('collection')
        verbose_name_plural = _('collections')

    @property
    def _related_objects(self) -> list:
        """Return list of related objects."""
        related_objects = []
        # get related objects
        for related_object in self._meta.related_objects:
            related_objects.append(related_object)
        return related_objects

    @property
    def count_related_objects(self) -> int:
        """Return count of related objects."""
        counter = 0
        # count of related objects
        for related_object in [related_object.name for related_object in self._related_objects]:
            counter += getattr(self, f'{related_object}').count()
        return counter

    @property
    def related_object_names(self) -> list:
        """Return related object names."""
        raw_objects = []
        for related_object in [related_object.name for related_object in self._related_objects]:
            instances = getattr(self, f'{related_object}')
            if instances.exists():
                for instance in instances.all():
                    raw_object = (instance.id, instance.establishment_type.index_name,
                                  instance.slug) if \
                        hasattr(instance, 'slug') else (instance.id, None, None)
                    raw_objects.append(raw_object)

        # parse slugs
        related_objects = []
        object_names = set()
        re_pattern = r'[\w]+'
        for object_id, object_type, raw_name, in raw_objects:
            result = re.findall(re_pattern, raw_name)
            if result:
                name = ' '.join(result).capitalize()
                if name not in object_names:
                    related_objects.append({
                        'id': object_id,
                        'establishment_type': object_type,
                        'name': name
                    })
                object_names.add(name)

        return related_objects

    def save(self, *args, **kwargs):
        if not self.pk:
            slugify_slug = slugify(
                next(iter(self.name.values())),
                word_boundary=True
            )
            self.slug = slugify_slug
        super(Collection, self).save(*args, **kwargs)


class GuideQuerySet(models.QuerySet):
    """QuerySet for Guide."""

    def with_base_related(self):
        """Return QuerySet with related."""
        return self.select_related('guide_type', 'site')

    def by_country_id(self, country_id):
        """Return QuerySet filtered by country code."""
        return self.filter(country_json__id__contains=country_id)

    def annotate_in_restaurant_section(self):
        """Annotate flag if GuideElement in RestaurantSectionNode."""
        return self.annotate(
            in_restaurant_section=models.Case(
                models.When(
                    guideelement__guide_element_type__name='EstablishmentNode',
                    guideelement__parent__guide_element_type__name='RestaurantSectionNode',
                    then=True),
                default=False,
                output_field=models.BooleanField(default=False)
            )
        )

    def annotate_in_shop_section(self):
        """Annotate flag if GuideElement in ShopSectionNode."""
        return self.annotate(
            in_shop_section=models.Case(
                models.When(
                    guideelement__guide_element_type__name='EstablishmentNode',
                    guideelement__parent__guide_element_type__name='ShopSectionNode',
                    then=True),
                default=False,
                output_field=models.BooleanField(default=False)
            )
        )

    def annotate_restaurant_counter(self):
        """Return QuerySet with annotated field - restaurant_counter."""
        return self.annotate_in_restaurant_section().annotate(
            restaurant_counter=models.Count(
                'guideelement',
                filter=models.Q(in_restaurant_section=True) &
                       models.Q(guideelement__parent_id__isnull=False),
                distinct=True))

    def annotate_shop_counter(self):
        """Return QuerySet with annotated field - shop_counter."""
        return self.annotate_in_shop_section().annotate(
            shop_counter=models.Count(
                'guideelement',
                filter=models.Q(in_shop_section=True) &
                       models.Q(guideelement__parent_id__isnull=False),
                distinct=True))

    def annotate_wine_counter(self):
        """Return QuerySet with annotated field - shop_counter."""
        return self.annotate_in_restaurant_section().annotate(
            wine_counter=models.Count(
                'guideelement',
                filter=models.Q(guideelement__guide_element_type__name='WineNode') &
                       models.Q(guideelement__parent_id__isnull=False),
                distinct=True))

    def annotate_present_objects_counter(self):
        """Return QuerySet with annotated field - present_objects_counter."""
        return self.annotate_in_restaurant_section().annotate(
            present_objects_counter=models.Count(
                'guideelement',
                filter=models.Q(guideelement__guide_element_type__name__in=['EstablishmentNode', 'WineNode']) &
                       models.Q(guideelement__parent_id__isnull=False),
                distinct=True))


class Guide(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin):
    """Guide model."""
    BUILT = 0
    WAITING = 1
    REMOVING = 2
    BUILDING = 3

    STATE_CHOICES = (
        (BUILT, 'built'),
        (WAITING, 'waiting'),
        (REMOVING, 'removing'),
        (BUILDING, 'building'),
    )

    RESTAURANT = 0
    ARTISAN = 1
    WINE = 2

    GUIDE_TYPE_CHOICES = (
        (RESTAURANT, _('Restaurant')),
        (ARTISAN, _('Artisan')),
        (WINE, _('Wine')),
    )

    guide_type = models.PositiveSmallIntegerField(choices=GUIDE_TYPE_CHOICES,
                                                  default=RESTAURANT,
                                                  verbose_name=_('guide type'))
    start = models.DateTimeField(null=True,
                                 verbose_name=_('start'))
    vintage = models.IntegerField(validators=[MinValueValidator(1900),
                                              MaxValueValidator(2100)],
                                  null=True,
                                  verbose_name=_('guide vintage year'))
    slug = models.SlugField(max_length=255, unique=True, null=True,
                            verbose_name=_('slug'))
    site = models.ForeignKey('main.SiteSettings', on_delete=models.SET_NULL,
                             null=True,
                             verbose_name=_('site settings'))
    state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
                                             verbose_name=_('state'))
    count_related_objects = models.PositiveIntegerField(
        default=0,
        help_text=_('* after rebuild guide, refresh count of related guide elements'),
        verbose_name=_('count of related guide elements during initialization'))
    old_id = models.IntegerField(blank=True, null=True)

    objects = GuideQuerySet.as_manager()

    class Meta:
        """Meta class."""
        verbose_name = _('guide')
        verbose_name_plural = _('guides')

    def __str__(self):
        """String method."""
        return f'{self.name}'

    def save(self, *args, **kwargs):
        if not self.pk:
            if not self.slug:
                slugify_slug = slugify(
                    f'{self.name} {self.vintage}',
                    word_boundary=True
                )
                self.slug = slugify_slug
        super(Guide, self).save(*args, **kwargs)

    # todo: for test use, use annotation instead
    # @property
    # def restaurant_counter_prop(self):
    #     counter = 0
    #     root_node = GuideElement.objects.get_root_node(self)
    #     if root_node:
    #         descendants = GuideElement.objects.get_root_node(self).get_descendants()
    #         if descendants:
    #             restaurant_section_nodes = descendants.filter(
    #                 guide_element_type__name='RestaurantSectionNode')
    #             counter = descendants.filter(guide_element_type__name='EstablishmentNode',
    #                                          parent__in=restaurant_section_nodes,
    #                                          parent_id__isnull=True).count()
    #     return counter
    #
    # @property
    # def shop_counter_prop(self):
    #     counter = 0
    #     root_node = GuideElement.objects.get_root_node(self)
    #     if root_node:
    #         descendants = GuideElement.objects.get_root_node(self).get_descendants()
    #         if descendants:
    #             shop_section_nodes = descendants.filter(
    #                 guide_element_type__name='ShopSectionNode')
    #             counter = descendants.filter(guide_element_type__name='EstablishmentNode',
    #                                          parent__in=shop_section_nodes,
    #                                          parent_id__isnull=True).count()
    #     return counter
    #
    # @property
    # def wine_counter_prop(self):
    #     counter = 0
    #     root_node = GuideElement.objects.get_root_node(self)
    #     if root_node:
    #         descendants = GuideElement.objects.get_root_node(self).get_descendants()
    #         if descendants:
    #             counter = descendants.filter(guide_element_type__name='WineNode',
    #                                          parent_id__isnull=True).count()
    #     return counter

    def update_count_related_objects(self, nodes: set = ('EstablishmentNode', 'WineNode')):
        """Update count of related guide element objects."""
        descendants = GuideElement.objects.get_root_node(self) \
                                          .get_descendants()
        if descendants:
            updated_count = descendants.filter(
                guide_element_type__name__in=nodes).count()
            self.count_related_objects = updated_count
            self.save()

    def generate_elements(self):
        if self.guidefilter:
            if self.guide_type in [self.ARTISAN, self.RESTAURANT]:
                if settings.USE_CELERY:
                    tasks.generate_establishment_guide_elements.delay(
                        guide_id=self.id,
                        filter_set=self.guidefilter.establishment_filter_set,
                    )
                else:
                    tasks.generate_establishment_guide_elements(
                        guide_id=self.id,
                        filter_set=self.guidefilter.establishment_filter_set,
                    )
            elif self.guide_type == self.WINE:
                if settings.USE_CELERY:
                    tasks.generate_product_guide_elements.delay(
                        guide_id=self.id,
                        filter_set=self.guidefilter.product_filter_set,
                    )
                else:
                    tasks.generate_product_guide_elements(
                        guide_id=self.id,
                        filter_set=self.guidefilter.product_filter_set,
                    )

    def regenerate_elements(self):
        # get Root node
        root_node = GuideElement.objects.get_root_node(self)
        # get all descendants related to this guide and delete it
        root_node.get_descendants().delete()
        # re-generate elements
        self.generate_elements()
        # update count elements
        self.update_count_related_objects()

    def change_state(self, state: int):
        self.state = state
        self.save()


class AdvertorialQuerySet(models.QuerySet):
    """QuerySet for model Advertorial."""


class Advertorial(ProjectBaseMixin):
    """Guide advertorial model."""
    number_of_pages = models.PositiveIntegerField(
        verbose_name=_('number of pages'),
        help_text=_('the total number of reserved pages'))
    right_pages = models.PositiveIntegerField(
        verbose_name=_('number of right pages'),
        help_text=_('the number of right pages (which are part of total number).'))
    guide_element = models.OneToOneField('GuideElement', on_delete=models.CASCADE,
                                         related_name='advertorial',
                                         verbose_name=_('guide element'))
    old_id = models.IntegerField(blank=True, null=True)

    objects = AdvertorialQuerySet.as_manager()

    class Meta:
        """Meta class."""
        verbose_name = _('advertorial')
        verbose_name_plural = _('advertorials')


class GuideFilterQuerySet(models.QuerySet):
    """QuerySet for model GuideFilter."""


class GuideFilter(ProjectBaseMixin):
    """Guide filter model."""

    establishment_type_json = JSONField(blank=True, null=True,
                                        verbose_name='establishment types')
    country_json = JSONField(blank=True, null=True,
                             verbose_name='countries')
    region_json = JSONField(blank=True, null=True,
                            verbose_name='regions')
    sub_region_json = JSONField(blank=True, null=True,
                                verbose_name='sub regions')
    wine_region_json = JSONField(blank=True, null=True,
                                 verbose_name='wine regions')
    with_mark = models.BooleanField(default=True,
                                    verbose_name=_('with mark'),
                                    help_text=_('exclude empty marks?'))
    locale_json = JSONField(blank=True, null=True,
                            verbose_name='locales')
    max_mark = models.FloatField(verbose_name=_('max mark'),
                                 null=True,
                                 help_text=_('mark under'))
    min_mark = models.FloatField(verbose_name=_('min mark'),
                                 null=True,
                                 help_text=_('mark over'))
    review_vintage_json = JSONField(verbose_name='review vintage years')
    review_state_json = JSONField(blank=True, null=True,
                                  verbose_name='review states')
    guide = models.OneToOneField(Guide, on_delete=models.CASCADE,
                                 verbose_name=_('guide'))
    old_id = models.IntegerField(blank=True, null=True)

    objects = GuideFilterQuerySet.as_manager()

    class Meta:
        """Meta class."""
        verbose_name = _('guide filter')
        verbose_name_plural = _('guide filters')

    def get_value_list(self, json_field: dict,
                             model: object,
                             lookup_field: str,
                             search_field: int = 'id') -> list:
        """
        Function to return an array with correct values from ids.
        Algorithm:
        1 Get values from json_field
        2 Try to find model instances by search field and value from json field
        3 If instance was found, then put value into array from instance by lookup field
        """

        value_list = []
        if hasattr(model, 'objects') and json_field:
            for value in getattr(json_field, 'get')(search_field):
                qs = model.objects.filter(**{search_field: value})
                if qs.exists():
                    value_list.append(getattr(qs.first(), lookup_field))
        return value_list

    @property
    def establishment_types(self):
        from establishment.models import EstablishmentType
        return self.get_value_list(json_field=self.establishment_type_json,
                                   model=EstablishmentType,
                                   lookup_field='index_name')

    @property
    def establishment_type_ids(self):
        from establishment.models import EstablishmentType
        return self.get_value_list(json_field=self.establishment_type_json,
                                   model=EstablishmentType,
                                   lookup_field='id')

    @property
    def locales(self):
        return self.get_value_list(json_field=self.locale_json,
                                   model=Language,
                                   lookup_field='locale')

    @property
    def review_states(self):
        states = []
        for state in self.review_state_json.get('state'):
            status_field = [field for field in Review._meta.fields
                                  if field.name == 'status'][0]
            status_field_id = Review._meta.fields.index(status_field)
            states.append(dict(Review._meta.fields[status_field_id].choices).get(state))
        return states

    @property
    def country_names(self):
        return self.get_value_list(json_field=self.country_json,
                                   model=Country,
                                   lookup_field='name_translated')

    @property
    def country_ids(self):
        return self.get_value_list(json_field=self.country_json,
                                   model=Country,
                                   lookup_field='id')

    @property
    def region_names(self):
        return self.get_value_list(json_field=self.region_json,
                                   model=Region,
                                   lookup_field='name')

    @property
    def region_ids(self):
        return self.get_value_list(json_field=self.region_json,
                                   model=Region,
                                   lookup_field='id')

    @property
    def sub_region_names(self):
        return self.get_value_list(json_field=self.sub_region_json,
                                   model=Region,
                                   lookup_field='name_translated')

    @property
    def sub_region_ids(self):
        return self.get_value_list(json_field=self.sub_region_json,
                                   model=Region,
                                   lookup_field='id')

    @property
    def wine_region_ids(self):
        return self.get_value_list(json_field=self.wine_region_json,
                                   model=WineRegion,
                                   lookup_field='id')

    @property
    def review_vintages(self):
        return self.review_vintage_json.get('vintage')

    @property
    def available_filters(self):
        filters = list()
        for i in self._meta.fields:
            if isinstance(i, JSONField):
                has_values = list(getattr(self, f'{i.name}').values())[0]
                if has_values:
                    filters.append(i.name)
        return filters

    @property
    def establishment_filter_set(self):
        filters = {
            # establishment.Establishment
            'public_mark__in': [self.min_mark, self.max_mark],
            # review.Reviews
            'reviews__vintage__in': self.review_vintages,
        }

        if self.establishment_type_ids:
            filters.update({
                # establishment.EstablishmentType
                'establishment_type_id__in': self.establishment_type_ids,
            })

        if self.country_ids:
            filters.update({
                # location.Country
                'address__city__country_id__in': self.country_ids,
            })

        if self.region_ids:
            filters.update({
                # location.Region
                'address__city__region__parent_id__in': self.region_ids,
            })

        if self.sub_region_ids:
            filters.update({
                # location.Region
                'address__city__region__parent_id__in': self.region_ids,
                'address__city__region_id__in': self.sub_region_ids,
            })

        if self.wine_region_ids:
            filters.update({
                # location.WineRegion
                'wine_region_id__in': self.wine_region_ids,
            })

        if self.with_mark:
            filters.update({
                # establishment.Establishment
                'public_mark__isnull': False,
            })

        if self.locales:
            filters.update({
                'reviews__text__has_any_keys': self.locales,
            })

        return filters

    @property
    def product_filter_set(self):
        filters = {
            # establishment.Establishment
            'establishment__public_mark__in': [self.min_mark, self.max_mark],
            # review.Reviews
            'reviews__vintage__in': self.review_vintages,
        }

        if self.establishment_type_ids:
            filters.update({
                # establishment.EstablishmentType
                'establishment__establishment_type_id__in': self.establishment_type_ids,
            })

        if self.country_ids:
            filters.update({
                # location.Country
                'establishment__address__city__country_id__in': self.country_ids,
            })

        if self.region_ids:
            filters.update({
                # location.Region
                'establishment__address__city__region__parent_id__in': self.region_ids,
            })

        if self.sub_region_ids:
            filters.update({
                # location.Region
                'establishment__address__city__region__parent_id__in': self.region_ids,
                'establishment__address__city__region_id__in': self.sub_region_ids,
            })

        if self.wine_region_ids:
            filters.update({
                # location.WineRegion
                'wine_origins__wine_region_id__in': self.wine_region_ids,
            })

        if self.with_mark:
            filters.update({
                # establishment.Establishment
                'establishment__public_mark__isnull': False,
            })

        if self.locales:
            filters.update({
                'reviews__text__has_any_keys': self.locales,
            })

        return filters


class GuideElementType(models.Model):
    """Model for type of guide elements."""

    name = models.CharField(max_length=50,
                            verbose_name=_('name'))

    class Meta:
        """Meta class."""
        verbose_name = _('guide element type')
        verbose_name_plural = _('guide element types')

    def __str__(self):
        """Overridden str dunder."""
        return self.name


class GuideWineColorSectionQuerySet(models.QuerySet):
    """QuerySet for model GuideWineColorSection."""


class GuideWineColorSection(ProjectBaseMixin):
    """Sections for wine colors."""

    name = models.CharField(max_length=255, verbose_name=_('section name'))

    objects = GuideWineColorSectionQuerySet.as_manager()

    class Meta:
        """Meta class."""
        verbose_name = _('guide wine color section')
        verbose_name_plural = _('guide wine color sections')


class GuideElementSectionCategoryQuerySet(models.QuerySet):
    """QuerySet for model GuideElementSectionCategory."""


class GuideElementSectionCategory(ProjectBaseMixin):
    """Section category for guide element."""

    name = models.CharField(max_length=255,
                            verbose_name=_('category name'))

    objects = GuideElementSectionCategoryQuerySet.as_manager()

    class Meta:
        """Meta class."""
        verbose_name = _('guide element section category')
        verbose_name_plural = _('guide element section categories')


class GuideElementSectionQuerySet(models.QuerySet):
    """QuerySet for model GuideElementSection."""


class GuideElementSection(ProjectBaseMixin):
    """Sections for guide element."""

    name = models.CharField(max_length=255, verbose_name=_('section name'))
    category = models.ForeignKey(GuideElementSectionCategory, on_delete=models.PROTECT,
                                 verbose_name=_('category'))
    old_id = models.PositiveIntegerField(blank=True, null=True, default=None,
                                         verbose_name=_('old id'))

    objects = GuideElementSectionQuerySet.as_manager()

    class Meta:
        """Meta class."""
        verbose_name = _('guide element section')
        verbose_name_plural = _('guide element sections')


class GuideElementManager(models.Manager):
    """Manager for model GuideElement."""

    def get_root_node(self, guide):
        """Return guide root element node."""
        qs = self.filter(guide=guide, guide_element_type__name='Root')
        if qs.exists():
            return qs.first()

    def get_or_create_root_node(self, guide_id: int):
        """Get or Create RootNode."""
        guide_element_type_qs = GuideElementType.objects.filter(name='Root')
        guide_qs = Guide.objects.filter(id=guide_id)

        if guide_element_type_qs.exists() and guide_qs.exists():
            guide = guide_qs.first()
            return self.get_or_create(guide_id=guide_qs.first(),
                                      guide_element_type=guide_element_type_qs.first(),
                                      defaults={
                                          'guide_id': guide.id,
                                          'guide_element_type': guide_element_type_qs.first()})
        return None, False

    def get_or_create_city_node(self, root_node_id: int, city_id: int):
        """Get or Create CityNode."""
        parent_node_qs = GuideElement.objects.filter(id=root_node_id)
        guide_element_type_qs = GuideElementType.objects.filter(name='CityNode')
        city_qs = City.objects.filter(id=city_id)

        if parent_node_qs.exists() and city_qs.exists() and guide_element_type_qs.exists():
            return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
                                      parent=parent_node_qs.first(),
                                      city=city_qs.first())
        return None, False

    def get_or_create_establishment_section_node(self, city_node: int, establishment_node_name: str):
        """Get or Create (Restaurant|Shop...)SectionNode."""
        parent_node_qs = GuideElement.objects.filter(id=city_node)
        guide_element_type_qs = GuideElementType.objects.filter(name__iexact=establishment_node_name)

        if parent_node_qs.exists() and guide_element_type_qs.exists():
            parent_node = parent_node_qs.first()
            return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
                                      parent=parent_node,
                                      guide=parent_node.get_root().guide)
        return None, False

    def get_or_create_establishment_node(self, restaurant_section_node_id: int,
                                         establishment_id: int, review_id: int = None):
        """Get or Create EstablishmentNode."""
        from establishment.models import Establishment

        data = {}
        guide_element_type_qs = GuideElementType.objects.filter(name='EstablishmentNode')
        parent_node_qs = GuideElement.objects.filter(id=restaurant_section_node_id)
        establishment_qs = Establishment.objects.filter(id=establishment_id)

        if parent_node_qs.exists() and establishment_qs.exists() and guide_element_type_qs.exists():
            establishment = establishment_qs.first()
            parent_node = parent_node_qs.first()
            data.update({
                'guide_element_type': guide_element_type_qs.first(),
                'parent': parent_node,
                'guide': parent_node.get_root().guide,
                'establishment': establishment
            })
            if review_id:
                review_qs = Review.objects.filter(id=review_id)
                if review_qs.exists(): data.update({'review_id': review_qs.first().id})
            return self.get_or_create(**data)
        return None, False

    def get_or_create_wine_region_node(self, root_node_id: int, wine_region_id: int):
        """Get or Create WineRegionNode."""
        guide_element_type_qs = GuideElementType.objects.filter(name='RegionNode')
        parent_node_qs = GuideElement.objects.filter(id=root_node_id)
        wine_region_qs = WineRegion.objects.filter(id=wine_region_id)

        if parent_node_qs.exists() and parent_node_qs.first().guide and wine_region_qs.exists() and guide_element_type_qs.exists():
            root_node = parent_node_qs.first()
            return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
                                      parent=root_node,
                                      guide=root_node.guide,
                                      wine_region=wine_region_qs.first())
        return None, False

    def get_or_create_yard_node(self, product_id: int, wine_region_node_id: int):
        """Make YardNode."""
        from establishment.models import Establishment

        guide_element_type_qs = GuideElementType.objects.filter(name='YardNode')
        wine_region_node_qs = GuideElement.objects.filter(id=wine_region_node_id)
        product_qs = Product.objects.filter(id=product_id)

        if product_qs.exists() and wine_region_node_qs.exists():
            wine_region_node = wine_region_node_qs.first()
            root_node = wine_region_node.get_root()
            return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
                                      parent=wine_region_node,
                                      guide=root_node.guide,
                                      product=product_qs.first())
        return None, False

    def get_or_create_color_wine_section_node(self, wine_color_name: str, yard_node_id: int):
        """Get or Create WineSectionNode."""
        guide_element_type_qs = GuideElementType.objects.filter(name='ColorWineSectionNode')
        parent_node_qs = GuideElement.objects.filter(id=yard_node_id)

        if not wine_color_name.endswith('SectionNode'):
            wine_color_name = transform_into_readable_str(wine_color_name)

        wine_color_section, _ = GuideWineColorSection.objects.get_or_create(
            name=wine_color_name,
            defaults={
                'name': wine_color_name
            })

        if parent_node_qs.exists() and guide_element_type_qs.exists():
            root_node = parent_node_qs.first().get_root()
            return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
                                      parent=root_node,
                                      wine_color_section=wine_color_section,
                                      guide=root_node.guide)
        return None, False

    def get_or_create_wine_node(self, color_wine_section_node_id: int, wine_id: int, review_id: int):
        """Get or Create WineNode."""
        guide_element_type_qs = GuideElementType.objects.filter(name='WineNode')
        parent_node_qs = GuideElement.objects.filter(id=color_wine_section_node_id)
        wine_qs = Product.objects.wines().filter(id=wine_id)
        review_qs = Review.objects.filter(id=review_id)

        if parent_node_qs.exists() and wine_qs.exists() and review_qs.exists() and guide_element_type_qs.exists():
            root_node = parent_node_qs.first().get_root()
            return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
                                      parent=root_node,
                                      product=wine_qs.first(),
                                      guide=root_node.guide,
                                      review=review_qs.first())
        return None, False


class GuideElementQuerySet(models.QuerySet):
    """QuerySet for model Guide elements."""

    def base_related(self):
        """Return QuerySet with base related."""
        return self.select_related(
            'guide_element_type',
            'establishment',
            'review',
            'wine_region',
            'product',
            'city',
            'wine_color_section',
            'section',
            'guide',
            'label_photo',
        )

    def restaurant_nodes(self):
        """Return GuideElement with type RestaurantSectionNode."""
        return self.filter(guide_element_type__name='RestaurantSectionNode')

    def shop_nodes(self):
        """Return GuideElement with type ShopSectionNode."""
        return self.filter(guide_element_type__name='ShopSectionNode')

    def wine_nodes(self):
        """Return GuideElement with type WineNode."""
        return self.filter(guide_element_type__name='WineNode')

    def descendants(self):
        """Return QuerySet with descendants."""
        return self.exclude(guide_element_type__name='Root')


class GuideElement(ProjectBaseMixin, MPTTModel):
    """Frozen state of elements of guide instance."""

    guide_element_type = models.ForeignKey('GuideElementType', on_delete=models.SET_NULL,
                                           null=True,
                                           verbose_name=_('guide element type'))
    establishment = models.ForeignKey('establishment.Establishment', on_delete=models.SET_NULL,
                                      null=True, blank=True, default=None)
    review = models.ForeignKey('review.Review', on_delete=models.SET_NULL,
                               null=True, blank=True, default=None)
    wine_region = models.ForeignKey('location.WineRegion', on_delete=models.SET_NULL,
                                    null=True, blank=True, default=None)
    product = models.ForeignKey('product.Product', on_delete=models.SET_NULL,
                                null=True, blank=True, default=None)
    priority = models.IntegerField(null=True, blank=True, default=None)
    city = models.ForeignKey('location.City', on_delete=models.SET_NULL,
                             null=True, blank=True, default=None)
    wine_color_section = models.ForeignKey('GuideWineColorSection', on_delete=models.SET_NULL,
                                           null=True, blank=True, default=None)
    section = models.ForeignKey('GuideElementSection', on_delete=models.SET_NULL,
                                null=True, blank=True, default=None)
    guide = models.ForeignKey('Guide', on_delete=models.SET_NULL,
                              null=True, blank=True, default=None)
    parent = TreeForeignKey('self', on_delete=models.CASCADE,
                            null=True, blank=True,
                            related_name='children')
    label_photo = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
                                    null=True, blank=True, default=None,
                                    verbose_name=_('label photo'))
    old_id = models.PositiveIntegerField(blank=True, null=True, default=None,
                                         verbose_name=_('old id'))

    objects = GuideElementManager.from_queryset(GuideElementQuerySet)()

    class Meta:
        """Meta class."""
        verbose_name = _('guide element')
        verbose_name_plural = _('guide elements')

    class MPTTMeta:
        order_insertion_by = ['guide_element_type']

    def __str__(self):
        """Overridden dunder method."""
        return self.guide_element_type.name if self.guide_element_type else self.id

    @property
    def advertorial_page(self):
        if self.advertorial:
            return self.advertorial.right_pages
