import logging
from collections import defaultdict
from enum import Enum
from operator import attrgetter
from typing import List, Type

from django_elasticsearch_dsl import Document
from elasticsearch_dsl import A, Q, Search

from django.utils import timezone

from mentor.staff.loaders import StaffDismissedProfileLoader
from mentor.users.enums import NotificationEvent

from .documents import MentorDocument
from .models import MenteeFeedback, Mentor, MentorFeedback, Mentorship, Position
from .schemas.search import (
    MentorSearchIn,
    MentorSearchSuggestAllIn,
    MentorSearchSuggestAllOut,
    MentorSearchSuggestIn,
    MentorSearchSuggestOut,
    MinMaxFilterIn,
)

logger = logging.getLogger(__name__)


class UserType(str, Enum):
    MENTOR = "mentor"
    MENTEE = "mentee"


def update_mentor_mentees_count(pk: int) -> None:
    mentees_count = Mentorship.objects.filter(
        mentor_id=pk,
        status__in=[
            Mentorship.Status.ACCEPTED,
            Mentorship.Status.PAUSED,
            Mentorship.Status.COMPLETED,
        ],
    ).count()

    Mentor.objects.filter(pk=pk).update(mentees_count=mentees_count)


def update_mentor_feedback_count(pk: int) -> None:
    feedback_count = MentorFeedback.objects.visible().filter(mentor_id=pk).count()
    Mentor.objects.filter(pk=pk).update(feedback_count=feedback_count)


def has_active_mentorship(mentee_id: int, mentor_id: int) -> bool:
    return Mentorship.objects.filter(
        mentee_id=mentee_id,
        mentor_id=mentor_id,
        status__in=[
            Mentorship.Status.CREATED,
            Mentorship.Status.ACCEPTED,
            Mentorship.Status.PAUSED,
        ],
    ).exists()


Status = Mentorship.Status


STATUS_TRANSITIONS = {
    Status.CREATED: {
        "to": [Status.ACCEPTED, Status.DECLINED],
        "user_field": "mentor.user_id",
    },
    Status.ACCEPTED: {
        "to": [Status.PAUSED, Status.COMPLETED],
    },
    Status.DECLINED: {"to": [Status.ACCEPTED], "user_field": "mentor.user_id"},
    Status.PAUSED: {"to": [Status.ACCEPTED], "user_field": "status_by_id"},
    Status.COMPLETED: {
        "to": [],
    },
}


class TransitionCheckResult(Enum):
    NOT_ALLOWED = 1
    UNACCEPTABLE = 2
    OK = 3


def check_transition(
    request, mentorship: Mentorship, to_status: Status
) -> TransitionCheckResult:
    transition = STATUS_TRANSITIONS[mentorship.status]

    if to_status not in transition["to"]:
        return TransitionCheckResult.UNACCEPTABLE

    user_field = transition.get("user_field")
    if user_field and attrgetter(user_field)(mentorship) != request.user.id:
        return TransitionCheckResult.NOT_ALLOWED

    return TransitionCheckResult.OK


def set_accepted_date(mentorship: Mentorship) -> None:
    if mentorship.status == Status.ACCEPTED:
        mentorship.accepted_date = timezone.now()


def set_created_date(mentorship: Mentorship) -> None:
    if mentorship.status == Status.COMPLETED:
        mentorship.completed_date = timezone.now()


def update_staff_profiles() -> None:
    mentor_uids = Mentorship.objects.values_list(
        "mentor__user__yauid", flat=True
    ).distinct()
    mentee_uids = Mentorship.objects.values_list("mentee__yauid", flat=True).distinct()

    uniq_uids = {*mentor_uids, *mentee_uids}

    StaffDismissedProfileLoader(list(uniq_uids)).update()


def get_range_kwargs(value: MinMaxFilterIn, units: str = "y") -> dict:
    """
    Возвращает параметры для range-фильтра

    Фильтр по периоду дат:
      на входе: числа min/max
      на выходе: диапазон относительно `now`
    """
    kwargs = {}

    min_bound: int = value.min
    max_bound: int = value.max

    # если max будет больше min, то делаем разворот
    if (min_bound and max_bound) and max_bound < min_bound:
        max_bound, min_bound = min_bound, max_bound

    if max_bound and max_bound > 0:
        kwargs["gte"] = f"now-{max_bound}{units}-1y+1d/d"

    if min_bound:
        kwargs["lte"] = f"now-{min_bound}{units}/d" if min_bound > 0 else "now/d"

    return kwargs


def get_search_query(search: MentorSearchIn):
    """
    Возвращает подготовленный поисковый запрос
    """
    # query
    s = MentorDocument.search()

    if search.query:
        s = s.query(
            "multi_match",
            type="bool_prefix",
            query=search.query,
            fields=[
                "username^2",
                "first_name^2",
                "last_name^2",
                "position.name",
                "city.name",
                "skills.name",
                "departments.name",
            ],
        )
    else:
        s = s.query("match_all")

    # filters
    filters = search.filters
    if not filters:
        logger.info(f"search query:\n {s.to_dict()}")
        return s

    if getattr(filters, "cities", None):
        s = s.filter("terms", city__id=filters.cities)

    if getattr(filters, "skills", None):
        s = s.filter("terms", skills__id=filters.skills)

    if getattr(filters, "departments", None):
        s = s.filter("terms", departments__id=filters.departments)

    if getattr(filters, "positions", None):
        s = s.filter("terms", position__id=filters.positions)

    if getattr(filters, "employment", None):
        s = s.filter("terms", employment__id=filters.employment)

    if getattr(filters, "education", None):
        s = s.filter("terms", education__id=filters.education)

    if getattr(filters, "years_in_yandex", None):
        range_kwargs = get_range_kwargs(filters.years_in_yandex)
        if range_kwargs:
            s = s.filter("range", joined_at=range_kwargs)

    if getattr(filters, "years_in_carrier", None):
        range_kwargs = get_range_kwargs(filters.years_in_carrier)
        if range_kwargs:
            s = s.filter("range", carrier_begin=range_kwargs)

    if getattr(filters, "is_chief", False):
        s = s.filter("term", is_chief=True)

    if getattr(filters, "has_feedback", False):
        s = s.filter("term", has_feedback=True)

    if getattr(filters, "has_active_mentorships", False):
        s = s.filter("term", has_active_mentorships=True)

    if getattr(filters, "has_completed_mentorships", False):
        s = s.filter("term", has_completed_mentorships=True)

    logger.info(f"search query:\n {s.to_dict()}")

    return s


def get_facet_values(
    search: MentorSearchIn, name: str, field: str, size: int = 30, **kwargs
) -> List[str]:
    """
    Возвращает ключи из фасета для поля

    Выполняет поисковый запрос, добавляя в него агрегацию по полю,
    и потом собирает оттуда ключи
    """
    s = get_search_query(search)
    aggr_terms = kwargs.pop("aggr_terms", {})
    a = A("terms", field=field, size=size, **aggr_terms)
    s.aggs.bucket(name, a)

    extra_filters = kwargs.pop("filters", [])
    for extra in extra_filters:
        s = s.filter(extra)

    # нужен только фасет, поэтому отключаем результаты поиска
    s = s.source(False)
    s = s[:0]

    logger.info("facet query:\n %r", s.to_dict())

    response = s.execute()
    found = []
    aggr = getattr(response.aggregations, name, None)
    if aggr and getattr(aggr, "buckets", None):
        for bucket in aggr.buckets:
            found.append(bucket.key)

    return found


def get_suggest_results(
    document: Type[Document],
    search: MentorSearchSuggestIn,
    name: str,
    field: str,
    size: int = 10000,
    **kwargs,
) -> List[MentorSearchSuggestOut]:
    """
    Возврвщает данные для саджеста

    При этом нужно либо передать список ids,
    либо этот список будет запрошен из основного поиского запроса
    """
    if search.ids:
        ids = search.ids
    else:
        # кол-во элементов в фасете,
        # если не задано берем count() из модели
        facet_size = kwargs.pop("facet_size", 0)

        if not facet_size:
            model = document.django.model
            facet_size = model.objects.count()

        facet_kwargs = {
            "size": facet_size,
        }

        ids = get_facet_values(
            search.copy(exclude={"ids", "text"}), name, field, **facet_kwargs
        )

    if not ids:
        return []

    # query
    s = document.search()

    if search.text:
        s = s.query("match_bool_prefix", name=search.text)
    else:
        s = s.query("match_all")

    # filter
    s = s.filter("ids", values=ids)

    # pagination
    if search.size:
        size = search.size

    s = s[:size]

    # extra
    extra_filters = kwargs.get("filters", [])
    for extra in extra_filters:
        s = s.filter(extra)

    logger.info(f"suggest query:\n {s.to_dict()}")

    response = s.execute()
    logger.debug("suggest response: %r", response)

    return [MentorSearchSuggestOut(id=r.meta.id, text=r.name) for r in response]


def get_common_suggest_results(
    search: MentorSearchSuggestAllIn, size_per_item: int = 10, **kwargs
) -> MentorSearchSuggestAllOut:
    s = Search()

    mentor_query = Q(
        "multi_match",
        type="bool_prefix",
        query=search.query,
        fields=["username^4", "first_name^2", "last_name"],
    )
    mentor_aggs = A(
        "filter",
        filter=Q("term", _index="mentors"),
        aggs={
            "mentors": A(
                "top_hits",
                size=size_per_item,
                _source=["username", "first_name", "last_name"],
            )
        },
    )
    s.aggs.bucket("mentors", mentor_aggs)

    # собираем общий запрос
    s = s.query(Q("bool", should=[mentor_query]))

    # в результатах нужна только агрегация
    s = s[:0]
    s = s.source(False)

    logger.info(f"common suggest query:\n {s.to_dict()}")

    response = s.execute()
    aggs = getattr(response, "aggregations", {})
    items = defaultdict(list)

    for name in dir(aggs):
        agg = getattr(aggs, name, None)

        if not agg:
            continue

        agg = getattr(agg, name, agg)

        for hit in agg.hits:
            items[name].append(
                {
                    "id": hit.meta.id,
                    **hit.to_dict(),
                }
            )

    return MentorSearchSuggestAllOut(**items)


def update_mentor_position(
    mentor: Mentor, position_name: str, commit: bool = True
) -> None:
    """
    Обновляет должность ментора по названию

    """
    position, created = Position.objects.get_or_create(
        name=position_name,
    )
    if mentor.position_id != position.pk:
        mentor.position = position
        if commit:
            mentor.save(update_fields=["position", "modified"])


def send_mentorship_notification(pk: int, event: NotificationEvent) -> None:
    """
    Отправка уведомлений по менторствам
    """
    from mentor.users.services import send_notification

    mentorship = Mentorship.objects.select_related(
        "mentor", "mentor__user", "mentee"
    ).get(pk=pk)
    user_mapping: dict = {
        NotificationEvent.MENTEE_CREATED_MENTORSHIP: mentorship.mentor.user,
        NotificationEvent.MENTEE_COMPLETED_MENTORSHIP: mentorship.mentor.user,
        NotificationEvent.MENTOR_ACCEPTED_MENTORSHIP: mentorship.mentee,
        NotificationEvent.MENTOR_DECLINED_MENTORSHIP: mentorship.mentee,
        NotificationEvent.MENTOR_COMPLETED_MENTORSHIP: mentorship.mentee,
    }
    context = {
        "mentorship": mentorship,
        "mentor": mentorship.mentor,
        "mentee": mentorship.mentee,
    }

    user = user_mapping.get(event.value)
    if user:
        send_notification(event, user, context=context)


def send_mentor_feedback_notification(pk: int) -> None:
    """
    Отправка уведомлений по отзывам на менторов
    """
    from mentor.users.services import send_notification

    feedback: MentorFeedback = (
        MentorFeedback.objects.select_related(
            "mentor",
            "mentor__user",
            "mentorship__mentee",
        )
        .filter(pk=pk, is_visible=True)
        .first()
    )
    if not feedback:
        return

    context = {
        "feedback": feedback,
        "mentor": feedback.mentor,
        "mentee": feedback.mentorship.mentee,
    }
    send_notification(
        NotificationEvent.MENTEE_LEFT_FEEDBACK,
        user=feedback.mentor.user,
        context=context,
    )


def send_mentee_feedback_notification(pk: int) -> None:
    """
    Отправка уведомлений по отзывам на ментии
    """
    from mentor.users.services import send_notification

    feedback: MenteeFeedback = (
        MenteeFeedback.objects.select_related(
            "mentee",
            "mentorship__mentor__user",
        )
        .filter(pk=pk, is_visible=True)
        .first()
    )

    if not feedback:
        return

    context = {
        "feedback": feedback,
        "mentor": feedback.mentorship.mentor.user,
        "mentee": feedback.mentee,
    }
    send_notification(
        NotificationEvent.MENTOR_LEFT_FEEDBACK,
        user=feedback.mentee,
        context=context,
    )
