import logging
import requests

from collections import defaultdict
from copy import deepcopy
from datetime import timedelta
from urllib.parse import urlparse

from celery import chain as celery_chain
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.files.base import ContentFile
from django.core.management import call_command
from django.template import loader
from django.utils import timezone

from intranet.femida.src.attachments.models import Attachment
from intranet.femida.src.candidates.bulk_upload.choices import CANDIDATE_UPLOAD_MODES
from intranet.femida.src.candidates.choices import (
    ATTACHMENT_TYPES,
    DUPLICATION_CASE_STATUSES,
    REFERENCE_EVENTS,
    REFERENCE_EVENTS_TRANSLATIONS,
    SUBMISSION_STATUSES,
)
from intranet.femida.src.candidates.consideration_issues.base import IssueTypesRegistry
from intranet.femida.src.candidates.models import (
    Candidate,
    CandidateAttachment,
    CandidateSubmission,
    Challenge,
    Consideration,
    ConsiderationIssue,
    DuplicationCase,
    Reference,
    Rotation,
    Verification,
)
from intranet.femida.src.celery_app import app, beamery_app, get_retry_countdown
from intranet.femida.src.offers.choices import OFFER_STATUSES
from intranet.femida.src.offers.models import Offer
from intranet.femida.src.startrek.operations import IssueTransitionOperation, IssueUpdateOperation
from intranet.femida.src.startrek.utils import (
    add_comment,
    get_issue,
    ResolutionEnum,
    StatusEnum,
    TransitionEnum,
)
from intranet.femida.src.utils.lock import locked_task
from intranet.femida.src.utils.queryset import queryset_iterator

logger = logging.getLogger(__name__)
User = get_user_model()


def search_for_duplicate(candidate):
    from intranet.femida.src.candidates import deduplication
    duplicates = deduplication.DuplicatesFinder().find(
        candidate=candidate,
        strategy=deduplication.strategies.doubtful_strategy,
    )
    deduplication.create_duplication_cases(candidate, duplicates)


@app.task
def run_duplicates_search_for_candidate(candidate_id):
    candidate = Candidate.unsafe.alive().filter(id=candidate_id).first()
    if candidate is None:
        logger.info('Unable to find the candidate for checking for duplicates')
        return
    search_for_duplicate(candidate)


@app.task
@locked_task
def run_duplicates_search(delta=None):
    """
    Запустить процесс поиска дубликатов
    """
    from intranet.femida.src.candidates.helpers import get_candidates_modified_after_dt

    delta = delta or timedelta(days=1)
    from_dt = timezone.now() - delta
    candidates = (
        get_candidates_modified_after_dt(from_dt)
        .prefetch_related(
            'contacts',
            'candidate_attachments__attachment',
        )
        .order_by('id')
    )
    first_candidate = candidates.first()
    if first_candidate is None:
        logger.info('No changes in %s. Skip duplicates search.', delta)
        return
    from_pk = first_candidate.id
    for candidate in queryset_iterator(candidates, from_pk=from_pk):
        search_for_duplicate(candidate)


@app.task
def auto_merge_duplicates():
    """
    Запустить процесс автоматического склеивания кандидатов
    """
    from intranet.femida.src.candidates import deduplication
    duplication_cases = (
        DuplicationCase.unsafe
        .filter(status=DUPLICATION_CASE_STATUSES.new, is_auto_merge=True)
    )
    cand_map = defaultdict(set)
    for case in duplication_cases:
        if case.is_auto_merge:
            cand_map[case.first_candidate_id].add(case.second_candidate_id)
            cand_map[case.second_candidate_id].add(case.first_candidate_id)

    new_cand_map = {}

    for cand_id, duplicate_ids in cand_map.items():
        cand_ids = duplicate_ids | {cand_id}
        updated_cand_map = {c: new_cand_map.get(c, c) for c in cand_ids}

        candidates = (
            Candidate.unsafe.alive()
            .filter(id__in=updated_cand_map.values())
        )
        processed_cand_ids = set(candidates.values_list('id', flat=True))

        if len(processed_cand_ids) == 1:
            continue

        if candidates:
            new_candidate = deduplication.CandidateMerger(candidates).merge()
            for old_cand_id, new_cand_id in updated_cand_map.items():
                if new_cand_id in processed_cand_ids:
                    new_cand_map[old_cand_id] = new_candidate.id


@app.autoretry_task(max_retries=3)
@locked_task
def fetch_crypta_vectors(submission_id=None):
    call_command('fetch_crypta_vectors', submission_id=submission_id)


@app.autoretry_task(max_retries=3)
@locked_task
def seo_check(submission_id=None):
    call_command('seo_check', submission_id=submission_id)


@app.autoretry_task(max_retries=3)
def auto_handle_internship_submission(submission_id):
    from intranet.femida.src.candidates.submissions.internships import handle_internship_submission

    submission = CandidateSubmission.unsafe.get(id=submission_id)
    handle_internship_submission(submission)


@app.task
@locked_task
def sync_contests_results():
    from intranet.femida.src.candidates.submissions.internships import sync_actual_contests_results

    sync_actual_contests_results()


@app.task
@locked_task
def sync_contest_participant_ids_task():
    from intranet.femida.src.candidates.submissions.internships import sync_contest_participant_ids

    sync_contest_participant_ids()


@app.task
@locked_task
def sync_contests_meta_info_task():
    from intranet.femida.src.contest.helpers import sync_contests_meta_info

    sync_contests_meta_info()


@app.autoretry_task(max_retries=5)
def reupload_file_to_mds_task(submission_id):
    from intranet.femida.src.forms_constructor.controllers import reupload_file_to_mds

    submission = CandidateSubmission.unsafe.get(id=submission_id)
    attachment_url = submission.form_cv_url
    if attachment_url:
        if submission.attachment is None:
            attachment = reupload_file_to_mds(attachment_url)
            submission.attachment = attachment
            # Перезаливка резюме из MDS может выполняться параллельно с обработкой отклика,
            # поэтому избегаем конкурирующей записи с помощью update_fields.
            submission.save(update_fields=['attachment', 'modified'])
        else:
            logger.warning('Attachment was already created for submission %d', submission.id)


@app.autoretry_task(max_retries=3)
def create_ess_issue_task(verification_id):
    from intranet.femida.src.candidates.startrek.issues import create_ess_issue
    verification = (
        Verification.objects
        .select_related(
            'application__vacancy__department',
            'application__vacancy__profession',
            'application__vacancy__professional_sphere',
            'candidate',
            'created_by',
        )
        .get(id=verification_id)
    )
    if verification.startrek_ess_key:
        logger.warning(
            'ESS issue was already created for verification %s: %s',
            verification.id,
            verification.startrek_ess_key,
        )
        return
    create_ess_issue(verification)


@app.autoretry_task(max_retries=3)
def resolve_verification_task(verification_id):
    from intranet.femida.src.candidates.verifications.controllers import (
        handle_verification_resolution,
    )
    verification = (
        Verification.objects
        .select_related('candidate')
        .get(id=verification_id)
    )
    ess_issue = get_issue(verification.startrek_ess_key)

    if ess_issue.status.key != StatusEnum.closed:
        raise Exception(
            'ESS issue %s is not in correct status: %s' % (
                ess_issue.key,
                ess_issue.status.key,
            )
        )
    handle_verification_resolution(verification, ess_issue.resolution.key)


# Рекомендации
def get_candidate_alive_references_qs(candidate_id):
    return (
        Reference.objects
        .alive()
        .filter(submission__candidate_id=candidate_id)
    )


@app.autoretry_task(max_retries=5)
def send_reference_event_task(reference_id, event, initiator_id=None, **kwargs):
    """
    Отправляем в REFERENCE-тикет событие по конкретной рекомендации
    """
    reference = (
        Reference.objects
        .select_related('submission__responsible')
        .prefetch_related('submission__candidate__responsibles')
        .get(id=reference_id)
    )
    comment = REFERENCE_EVENTS_TRANSLATIONS[event]
    if event == REFERENCE_EVENTS.consideration_archived:
        issue = get_issue(reference.startrek_key)
        comment = loader.render_to_string(
            template_name='startrek/references/reference-candidate-closed.wiki',
            context={'recruiter': reference.submission.responsible},
        )
        if issue.status.key != StatusEnum.closed:
            IssueTransitionOperation(reference.startrek_key)(
                transition=TransitionEnum.close,
                resolution=ResolutionEnum.fixed,
                comment=comment,
            )
            return
    elif event == REFERENCE_EVENTS.consideration_created:
        issue = get_issue(reference.startrek_key)
        if issue.status.key == StatusEnum.closed:
            initiator = User.objects.get(id=initiator_id) if initiator_id else None
            comment = loader.render_to_string(
                template_name='startrek/references/reference-candidate-opened.wiki',
                context={'initiator': initiator},
            )
            IssueTransitionOperation(reference.startrek_key)(
                transition=TransitionEnum.in_progress,
                assignee=reference.submission.candidate.main_recruiter.username,
                comment=comment,
            )
            return
    add_comment(reference.startrek_key, comment)


@app.autoretry_task(max_retries=3)
def send_candidate_reference_event_task(candidate_id, event, initiator_id=None):
    """
    Отправляем события в REFERENCE-тикеты всех активных рекомендаций
    """
    qs = get_candidate_alive_references_qs(candidate_id)
    reference_ids = qs.values_list('id', flat=True)
    for _id in reference_ids:
        send_reference_event_task.delay(_id, event, initiator_id)


@app.autoretry_task(max_retries=3)
def resolve_reference_issue_task(reference_id=None, grade=None, candidate_id=None, **fields):
    from intranet.femida.src.candidates.helpers import get_reference_award_tag

    assert grade
    if candidate_id:
        resolve_candidate_reference_issue_task.delay(
            candidate_id=candidate_id,
            grade=grade,
            **fields,
        )
        return
    reference = Reference.objects.get(id=reference_id)
    comment = REFERENCE_EVENTS_TRANSLATIONS[REFERENCE_EVENTS.offer_accepted]
    award_tag = get_reference_award_tag(grade, reference.status)
    IssueTransitionOperation(reference.startrek_key).delay(
        transition=TransitionEnum.resolve,
        comment=comment,
        tags={'add': [award_tag]},
        **fields
    )


@app.autoretry_task(max_retries=3)
def resolve_candidate_reference_issue_task(candidate_id, grade, **fields):
    qs = get_candidate_alive_references_qs(candidate_id)
    reference_ids = qs.values_list('id', flat=True)
    for _id in reference_ids:
        resolve_reference_issue_task.delay(_id, grade, **fields)


@app.autoretry_task(max_retries=3)
def finish_reference_issue_task(candidate_id, **fields):
    """
    Переводит тикеты REFERENCE для активных рекомендаций кандидата
    в статус "Добавлен на Стафф"
    """
    references = get_candidate_alive_references_qs(candidate_id)
    reference_keys = references.values_list('startrek_key', flat=True)
    comment = REFERENCE_EVENTS_TRANSLATIONS[REFERENCE_EVENTS.offer_closed]

    for key in reference_keys:
        IssueTransitionOperation(key).delay(
            transition=TransitionEnum.added_to_staff,
            comment=comment,
            **fields
        )

    # Обнуляем expiration_date после успешного вывода сотрудника по стаффу, так как по
    # сотруднику может вновь начаться работа менее чем через 6 мес. после вывода, и мы начнём
    # слать отбивки в старые тикеты REFERENCE.
    references.update(expiration_date=timezone.now())


@app.autoretry_task(max_retries=3)
def reopen_reference_issue_task(candidate_id):
    qs = get_candidate_alive_references_qs(candidate_id)
    reference_keys = qs.values_list('startrek_key', flat=True)
    for key in reference_keys:
        IssueTransitionOperation(key).delay(
            transition=TransitionEnum.in_progress,
            tags={'remove': [tag for _, tag in settings.REFERENCE_GRADE_TO_AWARD_TAG]},
            legalEntity=None,
            start=None,
        )


@app.autoretry_task(max_retries=5)
def create_reference_issue_task(reference_id):
    """
    Если не вышло создать тикет синхронно, пробуем сделать это отложенно.
    """
    from intranet.femida.src.candidates.startrek.issues import create_reference_issue
    reference = (
        Reference.objects
        .select_related(
            'submission__attachment',
            'created_by',
        )
        .prefetch_related(
            'submission__professions',
            'submission__skills',
            'submission__target_cities',
        )
        .get(id=reference_id)
    )
    if reference.startrek_key:
        logger.warning(
            'REFERENCE issue was already created for reference %s: %s',
            reference_id,
            reference.startrek_key,
        )
        return
    create_reference_issue(reference)


@app.autoretry_task(max_retries=3)
def start_reference_issue_task(submission_id):
    submission = CandidateSubmission.unsafe.get(id=submission_id)
    recruiter = submission.candidate.main_recruiter
    context = {
        'user': recruiter,
    }
    comment = loader.render_to_string('startrek/references/start-progress.wiki', context)
    IssueTransitionOperation(submission.reference.startrek_key).delay(
        transition=TransitionEnum.in_progress,
        assignee=recruiter.username,
        comment=comment,
    )


@app.autoretry_task(max_retries=3)
def change_reference_issue_join_at_task(candidate_id, start):
    qs = get_candidate_alive_references_qs(candidate_id)
    reference_keys = qs.values_list('startrek_key', flat=True)
    for key in reference_keys:
        IssueUpdateOperation(key).delay(start=start)


@app.autoretry_task(max_retries=3)
def update_reference_issues_with_candidate_recruiters(candidate_id, main_recruiter=None,
                                                      recruiters=None):
    keys = []
    submissions = (
        CandidateSubmission.unsafe
        .filter(candidate_id=candidate_id, status=SUBMISSION_STATUSES.closed)
        .select_related('reference')
    )
    for submission in submissions:
        reference = submission.reference
        if reference:
            keys.append(reference.startrek_key)

    for key in keys:
        update_issue_with_candidate_recruiters.delay(key, main_recruiter, recruiters)


@app.autoretry_task(max_retries=3)
def actualize_reference_issues(candidate_id, initiator_id):
    references = get_candidate_alive_references_qs(candidate_id)
    accepted_offer = Offer.unsafe.filter(
        candidate=candidate_id,
        status=OFFER_STATUSES.accepted,
    ).first()
    for reference in references:
        issue = get_issue(reference.startrek_key)
        tasks = []
        if issue.status.key == StatusEnum.closed:
            tasks.append(send_reference_event_task.si(
                reference_id=reference.id,
                event=REFERENCE_EVENTS.consideration_created,
                initiator_id=initiator_id,
            ))
        if accepted_offer and issue.status.key != StatusEnum.resolved:
            tasks.append(resolve_reference_issue_task.si(
                reference_id=reference.id,
                grade=accepted_offer.grade,
                legalEntity=accepted_offer.org.startrek_id,
                start=accepted_offer.join_at.strftime('%Y-%m-%d'),
            ))
        if tasks:
            tasks_chain = celery_chain(tasks)
            tasks_chain.delay()


# Ротации


@app.autoretry_task(max_retries=5)
def create_rotation_issue_task(rotation_id, context, **kwargs):
    from intranet.femida.src.candidates.startrek.issues import create_rotation_issue
    rotation = Rotation.objects.get(id=rotation_id)
    if rotation.startrek_rotation_key:
        logger.warning(
            'ROTATION issue was already created for rotation %s: %s',
            rotation_id,
            rotation.startrek_rotation_key,
        )
        return
    create_rotation_issue(rotation, context, **kwargs)


@app.autoretry_task(max_retries=5)
def create_myrotation_issue_task(rotation_id, context, **kwargs):
    from intranet.femida.src.candidates.startrek.issues import create_myrotation_issue
    rotation = Rotation.objects.get(id=rotation_id)
    if rotation.startrek_myrotation_key:
        logger.warning(
            'MYROTATION issue was already created for rotation %s: %s',
            rotation_id,
            rotation.startrek_myrotation_key,
        )
        return
    create_myrotation_issue(rotation, context, **kwargs)


@app.autoretry_task(max_retries=3)
def rotation_approve_task(rotation_id):
    from intranet.femida.src.candidates.rotations.controllers import rotation_approve
    rotation = Rotation.objects.get(id=rotation_id)
    rotation_approve(rotation)


@app.autoretry_task(max_retries=3)
def rotation_reject_task(rotation_id):
    from intranet.femida.src.candidates.rotations.controllers import rotation_reject
    rotation = (
        Rotation.objects
        .select_related('created_by__department')
        .get(id=rotation_id)
    )
    rotation_reject(rotation)


@app.autoretry_task(max_retries=3)
def start_rotation_task(rotation_id):
    from intranet.femida.src.candidates.startrek.issues import start_rotation
    rotation = (
        Rotation.objects
        .select_related('submission__responsible')
        .get(id=rotation_id)
    )
    start_rotation(rotation)


@app.autoretry_task(max_retries=3)
def close_rotation_task(rotation_id):
    from intranet.femida.src.candidates.startrek.issues import close_rotation
    rotation = Rotation.objects.get(id=rotation_id)
    close_rotation(rotation)


@app.autoretry_task(max_retries=3)
def cancel_rotation_task(rotation_id):
    from intranet.femida.src.candidates.startrek.issues import cancel_rotation
    rotation = (
        Rotation.objects
        .select_related('created_by__department')
        .get(id=rotation_id)
    )
    cancel_rotation(rotation)


@app.autoretry_task(max_retries=4)
def yt_bulk_upload_candidates_task(table, mode, source=''):
    from intranet.femida.src.candidates.bulk_upload.uploaders import CandidateYTUploader
    uploader = CandidateYTUploader(table, mode=mode, source=source)
    uploader.upload()


@app.autoretry_task(max_retries=4)
def yt_bulk_upload_candidate_scorings_task(table, version, scoring_category):
    from intranet.femida.src.candidates.scorings.uploaders import CandidateScoringYTUploader
    uploader = CandidateScoringYTUploader(table, version, scoring_category)
    uploader.upload()


@app.autoretry_task(max_retries=3)
def update_rotation_issues_with_candidate_recruiters(candidate_id, main_recruiter=None,
                                                     recruiters=None):
    keys = []
    submissions = (
        CandidateSubmission.unsafe
        .filter(candidate_id=candidate_id, status=SUBMISSION_STATUSES.closed)
        .select_related('rotation')
    )
    for submission in submissions:
        rotation = submission.rotation
        if rotation:
            keys.append(rotation.startrek_rotation_key)
            keys.append(rotation.startrek_myrotation_key)

    for key in keys:
        update_issue_with_candidate_recruiters.delay(key, main_recruiter, recruiters)


@app.autoretry_task(max_retries=5)
def create_candidate_attachment_by_url_task(candidate_id, attachment_url):
    from intranet.femida.src.forms_constructor.controllers import reupload_file_to_mds

    parsed_url = urlparse(attachment_url)
    if parsed_url.netloc.endswith('.forms.yandex-team.ru'):
        attachment = reupload_file_to_mds(attachment_url)
    else:
        response = requests.get(attachment_url, timeout=30)
        if not response.ok:
            logger.warning(
                'Failed to get the file with response status %s: %s',
                response.status_code,
                response.reason,
            )
            response.raise_for_status()

        if parsed_url.path.split('.')[-1] != 'pdf':
            return
        file_name = 'yang_uploaded_attachment.pdf'
        attached_file = ContentFile(response.content, name=file_name)
        attachment = Attachment.objects.create(
            name=file_name,
            content_type=response.headers['Content-Type'],
            attached_file=attached_file,
        )

    has_attachment = (
        CandidateAttachment.unsafe
        .filter(
            candidate_id=candidate_id,
            attachment__sha1=attachment.sha1,
        )
        .exists()
    )
    if not has_attachment:
        CandidateAttachment.objects.create(
            attachment=attachment,
            candidate_id=candidate_id,
            type=ATTACHMENT_TYPES.resume,
        )


@app.autoretry_task(max_retries=3)
def update_issue_with_candidate_recruiters(key, main_recruiter=None, recruiters=None):
    issue = get_issue(key)
    if issue.status.key == StatusEnum.in_progress:
        fields = {}
        if main_recruiter:
            fields['assignee'] = main_recruiter
        if recruiters:
            fields['followers'] = {'add': recruiters}

        IssueUpdateOperation(key).delay(**fields)


@app.autoretry_task(max_retries=5)
def create_onedayoffer_issue_task(challenge_id):
    from intranet.femida.src.candidates.startrek.issues import create_onedayoffer_issue
    challenge = Challenge.objects.get(id=challenge_id)
    if challenge.startrek_onedayoffer_key:
        logger.warning(
            'ONEDAYOFFER issue was already created for challenge %s: %s',
            challenge_id,
            challenge.startrek_onedayoffer_key,
        )
        return
    create_onedayoffer_issue(challenge)


@app.autoretry_task(max_retries=5)
def send_consideration_onedayoffer_event_task(consideration_id, event):
    from intranet.femida.src.candidates.startrek.issues import send_onedayoffer_event
    challenges = Challenge.objects.onedayoffer().filter(consideration_id=consideration_id)
    send_onedayoffer_event(challenges, event)


@app.autoretry_task(max_retries=5)
def send_challenges_onedayoffer_event_task(challenge_ids, event):
    from intranet.femida.src.candidates.startrek.issues import send_onedayoffer_event
    challenges = Challenge.objects.filter(id__in=challenge_ids)
    send_onedayoffer_event(challenges, event)


@app.autoretry_task(max_retries=5)
def send_onedayoffer_challenge_results_task(challenge_id):
    from intranet.femida.src.candidates.startrek.issues import send_onedayoffer_challenge_results
    challenge = Challenge.objects.onedayoffer().filter(id=challenge_id).first()
    if challenge:
        send_onedayoffer_challenge_results(challenge)


@beamery_app.autoretry_task(max_retries=3)
def upload_from_beamery_task(data: list[dict]):
    """
    Загружает данные из Бимери в Фемиду.
    Таск ставится интегратором через YMQ.
    """
    from intranet.femida.src.candidates.bulk_upload.serializers import BeameryCandidateSerializer
    from intranet.femida.src.candidates.bulk_upload.uploaders import CandidateBeameryUploader

    # Копируем целиком, чтобы можно было менять, не меняя исходные аргументы таска
    data = BeameryCandidateSerializer(deepcopy(data), many=True).data

    to_create = []
    to_merge = []

    beamery_ids = {i['beamery_id'] for i in data}
    existing_candidates_qs = (
        Candidate.unsafe
        .filter(beamery_id__in=beamery_ids)
        .values_list('beamery_id', 'id')
    )
    beamery_to_femida_map = {str(beamery_id): id for beamery_id, id in existing_candidates_qs}

    for row in data:
        # Если не передан original, пытаемся ещё найти такого кандидата по beamery_id,
        # на случай, когда он создан через Бимери, но Фемида ещё не успела обратно сообщить свой id
        row['original'] = row['original'] or beamery_to_femida_map.get(row['beamery_id'])
        is_merge = bool(row['original'])
        if is_merge:
            to_merge.append(row)
        else:
            to_create.append(row)

    if to_create:
        uploader = CandidateBeameryUploader(CANDIDATE_UPLOAD_MODES.create, to_create)
        uploader.upload()

    if to_merge:
        uploader = CandidateBeameryUploader(CANDIDATE_UPLOAD_MODES.merge, to_merge)
        uploader.upload()


@app.autoretry_task(max_retries=3)
def upload_from_beamery_internal_task(data: list[dict]):
    """
    Дублирует логику upload_from_beamery_task, но только делает это
    через внутреннюю ("бесплатную") очередь
    """
    return upload_from_beamery_task(data)


@app.task(max_retries=3)
@locked_task
def update_consideration_issues_task(issue_type_names=None):
    issue_types = IssueTypesRegistry.get_issue_types()
    failed_names = []

    for issue_type_name in issue_type_names or issue_types:
        issue_type_class = issue_types[issue_type_name]
        try:
            issue_type_class().update_or_create_consideration_issues()
        except Exception:
            logger.exception(
                'Failed during update consideration_issues with type %s',
                issue_type_name,
            )
            failed_names.append(issue_type_name)

    if failed_names:
        update_consideration_issues_task.retry(
            countdown=get_retry_countdown(update_consideration_issues_task.request.retries),
            kwargs={
                'issue_type_names': failed_names,
            },
        )


@app.autoretry_task(max_retries=3)
def resolve_consideration_issue_task(issue_type_name, consideration_id, user_id=None):
    issue_type = IssueTypesRegistry.get_issue_type(issue_type_name)
    assert issue_type, issue_type_name
    now = timezone.now()
    issues = (
        ConsiderationIssue.objects
        .filter(
            type=issue_type_name,
            is_resolved=False,
            consideration_id=consideration_id,
        )
    )
    is_resolve_needed = (
        Consideration.unsafe
        .filter(id=consideration_id)
        .annotate(**issue_type.get_annotations())
        .annotate(issue_level=issue_type.get_issue_level_subquery())
        .filter(issue_level__isnull=True)
        .exists()
    )
    if is_resolve_needed:
        issues.update(is_resolved=True, resolved_at=now, resolved_by_id=user_id, modified=now)
