from copy import deepcopy
from datetime import datetime
from typing import List, Tuple

from model_utils import FieldTracker
from model_utils.models import TimeStampedModel
from ordered_model.models import OrderedModel, OrderedModelQuerySet
from simple_history.models import HistoricalRecords

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.postgres.fields import JSONField
from django.core.exceptions import ValidationError
from django.db import models, transaction
from django.template import Context, Template
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _

from lms.core.models.mixins import ActiveFilterMixin, ActiveModelMixin, HrdbIdModelMixin
from lms.courses.models import Course, CourseGroup, CourseStudent

from .survey.datasets import DATASETS
from .survey.validators import (
    DatasetFieldSchemaValidator, FieldSchemaValidator, MulticheckboxFieldSchemaValidator, NumberFieldSchemaValidator,
    SelectFieldSchemaValidator,
)
from .utils import flatten_options

User = get_user_model()


class EnrollFormQuerySet(ActiveFilterMixin, models.QuerySet):
    pass


class EnrollSurvey(TimeStampedModel, ActiveModelMixin):
    slug = models.SlugField(_("код"), max_length=255, unique=True)
    name = models.CharField(_("название"), max_length=255)
    summary = models.CharField(_("краткое описание"), max_length=500, blank=True)
    description = models.TextField(_("описание"), blank=True)

    created_by = models.ForeignKey(
        User,
        verbose_name=_("создано"),
        related_name='tracker_forms',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )

    objects = EnrollFormQuerySet.as_manager()

    class Meta:
        ordering = ('name',)
        verbose_name = _("Анкета при зачислении")
        verbose_name_plural = _("Анкеты при зачислении")

    def __str__(self):
        return f'{self.name} [{self.pk}]'


class EnrollSurveyField(OrderedModel, TimeStampedModel):
    survey = models.ForeignKey(
        EnrollSurvey,
        verbose_name=_("анкета"),
        related_name='fields',
        on_delete=models.CASCADE,
    )

    name = models.CharField(_("название поля"), max_length=255, help_text=_("Только латиница"))

    TYPE_TEXT = 'text'
    TYPE_TEXTAREA = 'textarea'
    TYPE_NUMBER = 'number'
    TYPE_URL = 'url'
    TYPE_EMAIL = 'email'
    TYPE_SELECT = 'select'
    TYPE_DATASET = 'dataset'
    TYPE_MULTICHECKBOX = 'multicheckbox'

    _TYPE_CHOICES = (
        (TYPE_TEXT, _("Текст")),
        (TYPE_TEXTAREA, _("Много текста")),
        (TYPE_NUMBER, _("Число")),
        (TYPE_URL, _("Ссылка")),
        (TYPE_EMAIL, _("Email")),
        (TYPE_SELECT, _("Выпадающий список")),
        (TYPE_DATASET, _("Список с набором данных")),
        (TYPE_MULTICHECKBOX, _("Несколько чекбоксов")),
    )

    TYPE_CHOICES_KEYS = [choice[0] for choice in _TYPE_CHOICES]

    field_type = models.CharField(_("тип"), max_length=20, choices=_TYPE_CHOICES, default=TYPE_TEXT)
    title = models.CharField(_("заголовок"), max_length=255)
    description = models.TextField(_("описание"), blank=True, help_text=_("Выводится под полем"))
    placeholder = models.CharField(_("подсказка"), max_length=255, blank=True, help_text=_("Выводится внутри поля"))
    parameters = JSONField(_("параметры"), default=dict, blank=True, null=True)
    default_value = models.CharField(
        _("по умолчанию"),
        max_length=255, blank=True,
        help_text=_("Значение по умолчанию"))
    is_required = models.BooleanField(_("обязательное поле"), default=False)
    is_hidden = models.BooleanField(_("скрыть"), default=False)

    order_with_respect_to = 'survey'

    @property
    def options(self):
        if self.field_type not in [self.TYPE_SELECT, self.TYPE_MULTICHECKBOX]:
            return {}

        option_list = self.parameters.get('options', [])

        if self.field_type == self.TYPE_MULTICHECKBOX:
            option_list = flatten_options(option_list)

        return {i.get('value'): i.get('content', '') for i in option_list if i.get('value')}

    @property
    def expanded_parameters(self) -> dict:
        expanded = deepcopy(self.parameters)
        if self.field_type == self.TYPE_DATASET:
            expanded['dataset']['url'] = f"{settings.DATASET_BASE_URL}{expanded['dataset']['name']}"

        return expanded

    class Meta:
        unique_together = ('survey', 'name')
        ordering = ('order',)
        verbose_name = _("поле для формы")
        verbose_name_plural = _("поля для формы")

    def __str__(self):
        return f'{self.title} [{self.name}]'

    TYPE_SCHEMA_VALIDATOR_MAP = {
        TYPE_NUMBER: NumberFieldSchemaValidator,
        TYPE_SELECT: SelectFieldSchemaValidator,
        TYPE_DATASET: DatasetFieldSchemaValidator,
        TYPE_MULTICHECKBOX: MulticheckboxFieldSchemaValidator,
    }

    def validate_parameters(self):
        if not isinstance(self.parameters, dict):
            raise ValidationError({'parameters': _('parameters is not dict')})

        errors = []
        validator = self.TYPE_SCHEMA_VALIDATOR_MAP.get(self.field_type, FieldSchemaValidator)()

        try:
            validator(self.parameters)
        except ValidationError as exc:
            errors.append(exc)

        if errors:
            raise ValidationError({'parameters': errors})

    def clean(self):
        if (self.is_hidden and self.is_required) and not self.default_value:
            raise ValidationError({
                'default_value': _("Для скрытого обязательного поля необходимо задать значение по умолчанию")
            })
        self.validate_parameters()

    def save(self, *args, **kwargs):
        self.clean()
        return super().save(*args, **kwargs)


class EnrollmentQuerySet(ActiveFilterMixin, OrderedModelQuerySet):
    def default(self):
        return self.filter(is_default=True).first()


class Enrollment(OrderedModel, TimeStampedModel, HrdbIdModelMixin):
    course = models.ForeignKey(
        Course,
        verbose_name=_("курс"),
        related_name='enrollments',
        on_delete=models.PROTECT,
    )

    course_name_template = models.CharField(
        _("шаблон названия курса"),
        max_length=255,
        blank=True,
    )

    survey = models.ForeignKey(
        EnrollSurvey,
        verbose_name=_("анкета при зачислении"),
        related_name='enrollments',
        on_delete=models.PROTECT,
        blank=True,
        null=True,
    )

    TYPE_INSTANT = 'instant'
    TYPE_MANUAL = 'manual'
    TYPE_TRACKER = 'tracker'

    _TYPE_CHOICES = (
        (TYPE_INSTANT, _("Автоматически")),
        (TYPE_MANUAL, _("Ручное")),
        (TYPE_TRACKER, _("Через Трекер")),
    )

    TYPE_CHOICES_KEYS = [choice[0] for choice in _TYPE_CHOICES]

    enroll_type = models.CharField(_("тип"), max_length=20, choices=_TYPE_CHOICES, default=TYPE_MANUAL)
    name = models.CharField(_("название"), max_length=255)
    summary = models.CharField(_("краткое описание"), max_length=500, blank=True)
    options = JSONField(_("параметры"), default=dict, blank=True)
    is_active = models.BooleanField(_("активно"), default=True)
    is_default = models.BooleanField(_("по умолчанию"), default=False)

    order_with_respect_to = 'course'

    objects = EnrollmentQuerySet.as_manager()

    class Meta:
        ordering = ('order',)
        verbose_name = _("метод зачисления")
        verbose_name_plural = _("методы зачисления")
        permissions = (
            ('import_enrollment', _('Can import enrollments')),
            ('export_enrollment', _('Can export enrollments')),
        )

    def __str__(self):
        return self.name

    def clean(self):
        default_exists_qs = Enrollment.objects.filter(
            course_id=self.course_id,
            is_default=True
        )

        if not self._state.adding:
            default_exists_qs = default_exists_qs.exclude(id=self.id)

        if not default_exists_qs.exists():
            self.is_default = True

    def save(self, *args, **kwargs):
        self.full_clean()
        return super().save(*args, **kwargs)

    @property
    def has_tracker_queues(self):
        return self.tracker_queues.exists()

    @property
    def has_enrolled_users(self):
        return self.enrolled_users.exists()

    def delete(self, *args, **kwargs):
        if self.has_tracker_queues:
            raise ValidationError(
                _("нельзя удалить метод зачисления, к которому привязаны очереди трекера"), code='invalid',
            )
        if self.has_enrolled_users:
            raise ValidationError(
                _("нельзя удалить метод зачисления, по которому созданы заявки"), code='invalid',
            )

        return super().delete(*args, **kwargs)


class EnrolledUserQuerySet(models.QuerySet):
    def pending(self):
        return self.filter(status=self.model.StatusChoices.PENDING)

    def enrolled(self):
        return self.filter(status=self.model.StatusChoices.ENROLLED)

    def pending_or_enrolled(self):
        return self.filter(status__in=self.model.USER_STATUS_KEYS)


class EnrolledUser(TimeStampedModel, HrdbIdModelMixin):
    course = models.ForeignKey(
        Course,
        verbose_name=_("курс"),
        related_name='enrolled_users',
        on_delete=models.PROTECT,
    )
    custom_course_name = models.CharField(
        _("название курса"),
        max_length=255,
        blank=True,
    )
    enrollment = models.ForeignKey(
        Enrollment,
        verbose_name=_("метод зачисления"),
        related_name='enrolled_users',
        on_delete=models.PROTECT,
        blank=True,
        null=True,
    )
    user = models.ForeignKey(
        User,
        verbose_name=_("пользователь"),
        related_name='enrolled_to',
        on_delete=models.CASCADE,
    )
    group = models.ForeignKey(
        CourseGroup,
        verbose_name=_("группа"),
        related_name='enrolled_users',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )
    survey = models.ForeignKey(
        EnrollSurvey,
        verbose_name=_("анкета при зачислении"),
        related_name='enrolled_users',
        on_delete=models.PROTECT,
        blank=True,
        null=True,
    )
    survey_data = JSONField(_("данные по анкете"), default=dict, blank=True)
    hrdb_survey_data = models.TextField(_("данные по анкете из hrdb"), blank=True)

    class StatusChoices(models.TextChoices):
        PENDING = 'pending', _("на рассмотрении")
        ENROLLED = 'enrolled', _("зачислен")
        REJECTED = 'rejected', _("отклонен")
        COMPLETED = 'completed', _("завершил обучение")

    USER_STATUS_KEYS = [StatusChoices.PENDING, StatusChoices.ENROLLED]

    status = models.CharField(
        _("статус"),
        max_length=20,
        choices=StatusChoices.choices,
        default=StatusChoices.PENDING,
        db_index=True,
    )
    comments = models.TextField(_("комментарии"), blank=True)

    groups = models.TextField(
        verbose_name=_("подразделения в момент подачи заявки"),
        help_text=_("строка подразделений работника в момент подачи заявки, разделенных /"),
        blank=True,
    )

    course_student = models.ForeignKey(
        CourseStudent,
        verbose_name=_("студент"),
        related_name='enrolled_users',
        on_delete=models.PROTECT,
        null=True,
        blank=True,
        help_text=_("студент, созданный по этой заявке"),
    )

    enroll_date = models.DateTimeField(
        verbose_name=_("дата подтверждения заявки"),
        null=True,
        blank=True,
        db_index=True,
    )
    completion_date = models.DateTimeField(
        verbose_name=_("дата завершения обучения"),
        null=True,
        blank=True,
        db_index=True,
    )

    objects = EnrolledUserQuerySet.as_manager()

    tracker = FieldTracker(fields=['status'])

    history = HistoricalRecords()

    @cached_property
    def _default_enrollment(self):
        return self.course.enrollments.default()

    TIME_TYPE_CANCELED = 'cancelled'
    TIME_TYPE_NO_DATE = 'no_date'
    TIME_TYPE_NO_PAST = 'past'
    TIME_TYPE_NO_FUTURE = 'future'

    @property
    def time_type(self):
        if self.status == EnrolledUser.StatusChoices.REJECTED:
            return EnrolledUser.TIME_TYPE_CANCELED
        elif not self.course.begin_date:
            return EnrolledUser.TIME_TYPE_NO_DATE
        if self.course.begin_date.date() < timezone.now().date():
            return EnrolledUser.TIME_TYPE_NO_PAST
        return EnrolledUser.TIME_TYPE_NO_FUTURE

    def get_group_or_course_field(self, field_name):
        return getattr(self.group, field_name, None) or getattr(self.course, field_name, None)

    @property
    def begin_date(self) -> datetime:
        if self.group and self.group.begin_date:
            return self.group.begin_date

        return self.course.begin_date

    @property
    def end_date(self) -> datetime:
        if self.group and self.group.end_date:
            return self.group.end_date

        return self.course.end_date

    @property
    def course_status(self):
        if self.course_student is not None:
            return self.course_student.status

        # В теории такой ситуации быть не должно, т.к. после перехода
        # заявки в статус ENROLLED в post_save сигнале создается CourseStudent
        if self.status == EnrolledUser.StatusChoices.ENROLLED:
            return CourseStudent.StatusChoices.ACTIVE

        return self.status

    def _get_survey_data_field_display(self, field: EnrollSurveyField) -> str:
        user_value = self.survey_data.get(field.name, None)
        value = user_value or field.default_value

        if field.field_type == field.TYPE_DATASET:
            if value:
                dataset = DATASETS[field.parameters['dataset']['name']]
                filter_kwargs = {dataset.lookup_field: value}
                value = dataset.get_queryset().filter(**filter_kwargs).first()

        if field.field_type == field.TYPE_SELECT:
            value = field.options.get(value, '')

        if field.field_type == field.TYPE_MULTICHECKBOX:
            values = []
            if user_value and isinstance(user_value, list):
                for val in user_value:
                    values.append(str(field.options.get(val, '')))

            value = ", ".join(values or [gettext("Любой")])

        return str(value)

    def _get_survey_data_fieldname_display(self, field_name: str) -> str:
        return self._get_survey_data_field_display(
            EnrollSurveyField.objects.filter(survey=self.survey, name=field_name).first(),
        )

    def generate_name(self):
        if not self.enrollment.course_name_template:
            return ''
        context = {
            'course': self.course,
            'group': self.group,
            'survey': self.survey_data,
        }
        context = Context(context)
        data = Template(self.enrollment.course_name_template).render(context)
        return str(data).strip()

    @property
    def course_name(self):
        return self.custom_course_name or self.course.name

    @property
    def user_answers(self) -> List[Tuple[str, str]]:
        results = []

        if not isinstance(self.survey_data, dict):
            return results

        for field in EnrollSurveyField.objects.filter(survey=self.survey):  # type: EnrollSurveyField
            value = self._get_survey_data_field_display(field)
            results.append((field.title, value))

        return results

    class Meta:
        ordering = ('-created',)
        verbose_name = _("заявка на зачисление")
        verbose_name_plural = _("заявки на зачисление")
        permissions = (
            ('import_enrolleduser', _('Can import enrolled users')),
            ('export_enrolleduser', _('Can export enrolled users')),
        )

    def clean(self):
        # устанавливаем дефолтный механизм зачисления
        if not self.enrollment:
            enrollment = self._default_enrollment
            if not enrollment:
                raise ValidationError({
                    "enrollment": _("Не найден дефолтный механизм зачисления")
                })
            self.enrollment = enrollment

        if self.enrollment.course_id != self.course_id:
            raise ValidationError({
                "enrollment": _("Механизм зачисления относится к другому курсу")
            })

        # проверка, что для курса с группами в заявке указана группа
        if self.group is None and self.course.enable_groups:
            raise ValidationError({
                "group": _("Для курса с группами должна быть указана группа"),
            })

        # проверка наличие указанной группы в курсе
        if self.group and self.group.course_id != self.course_id:
            raise ValidationError({
                "group": _("Группа не найдена в текущем курсе")
            })

        # проверка course_students
        if self.course_student:
            if self.user_id != self.course_student.user_id:
                raise ValidationError({
                    "course_student": _("у студента в заявке указан другой пользователь")
                })
            if self.course_id != self.course_student.course_id:
                raise ValidationError({
                    "course_student": _("у студента в заявке указан другой курс")
                })
            if self.group_id != self.course_student.group_id:
                raise ValidationError({
                    "course_student": _("у студента в заявке указана другая группа")
                })

        if self._state.adding:
            # указываем анкету курса
            self.survey = self.enrollment.survey

            # проверяем, открыта ли регистрация
            if (
                self.course.calc_enroll_begin is not None and
                timezone.now() <= self.course.calc_enroll_begin
            ):
                raise ValidationError({
                    "course": _("Регистрация еще не началась")
                })

            if (
                self.group is not None and
                self.group.enroll_begin is not None and
                timezone.now() <= self.group.enroll_begin
            ):
                raise ValidationError({
                    "group": _("Регистрация еще не началась")
                })

            if (
                self.course.calc_enroll_end is not None and
                self.course.calc_enroll_end <= timezone.now()
            ):
                raise ValidationError({
                    "course": _("Регистрация завершена")
                })

            if (
                self.group is not None and
                self.group.enroll_end is not None and
                self.group.enroll_end <= timezone.now()
            ):
                raise ValidationError({
                    "group": _("Регистрация завершена")
                })

            # доступна ли группа для записи
            if self.group is not None and not self.group.available:
                raise ValidationError({
                    "group": _("Группа недоступна для записи")
                })

    def change_status_on_save(self):
        if self.tracker.has_changed('status'):
            if self.status == self.StatusChoices.ENROLLED:
                self.enroll_date = timezone.now()
            if self.status == self.StatusChoices.COMPLETED:
                self.completion_date = timezone.now()

    def check_existing_enrollments(self):
        if self.course.multi_enrollments or self.status not in self.USER_STATUS_KEYS:
            return

        # ограничиваем создание заявок при отключенной мультизаписи
        active_statuses = [EnrolledUser.StatusChoices.PENDING, EnrolledUser.StatusChoices.ENROLLED]

        # при отключенном повторном прохождении нельзя подавать заявку даже после завершения курса
        if not self.course.retries_allowed:
            active_statuses.append(EnrolledUser.StatusChoices.COMPLETED)

        # проверяем существующие заявки
        with transaction.atomic():
            existing_enrollments = EnrolledUser.objects.select_for_update().filter(
                course=self.course,
                user=self.user,
                status__in=active_statuses
            )

            # Исключаем текущую заявку
            if self.pk:
                existing_enrollments = existing_enrollments.exclude(pk=self.pk)

            if existing_enrollments.exists():
                # если включено повторное прохождение и есть незавершенное обучение,
                # то выбрасываем об этом ошибку
                if self.course.retries_allowed:
                    has_active_student = CourseStudent.objects.active().filter(
                        course=self.course, user=self.user
                    ).exists()

                    if has_active_student:
                        raise ValidationError({
                            "user": _("перед подачей новой заявки сначала нужно завершить обучение в курсе")
                        })

                raise ValidationError({
                    "user": _("заявка на курс уже была подана")
                })

    @transaction.atomic
    def check_group_is_full(self):
        # проверяем наличие свободных мест
        if self.group_id:
            course_group = CourseGroup.objects.select_for_update().filter(id=self.group_id).first()
            if course_group and course_group.is_full:
                raise ValidationError({
                    "course": _("В группе не осталось свободных мест")
                })

    def generate_course_name(self):
        generated_course_name = self.generate_name()
        if generated_course_name:
            self.custom_course_name = generated_course_name

    def save(self, *args, **kwargs):
        self.full_clean()
        self.check_existing_enrollments()
        self.change_status_on_save()
        if self._state.adding:
            self.generate_course_name()
            self.check_group_is_full()
        return super().save(*args, **kwargs)

    def enroll(self, change_reason: str = None) -> None:
        if change_reason:
            self._change_reason = change_reason

        self.status = self.StatusChoices.ENROLLED
        self.save()

    def reject(self, change_reason: str = None) -> None:
        if change_reason:
            self._change_reason = change_reason

        self.status = self.StatusChoices.REJECTED
        self.save()

    def complete(self, change_reason: str = None) -> None:
        if change_reason:
            self._change_reason = change_reason

        self.status = self.StatusChoices.COMPLETED
        self.save()

    def __str__(self):
        return f"Заявка {self.pk} | Cтудент {self.user_id} | Курс {self.course_id}"
