import json
import logging
from collections import defaultdict

from constance import config
from django.db import transaction

from intranet.femida.src.actionlog.decorators import action_logged
from intranet.femida.src.applications.models import Application
from intranet.femida.src.candidates import choices, deduplication
from intranet.femida.src.candidates.challenges.workflow import ChallengeWorkflow
from intranet.femida.src.candidates.considerations.controllers import (
    update_consideration_extended_status,
)
from intranet.femida.src.candidates.models import Challenge, DuplicationCase
from intranet.femida.src.candidates.submissions.controllers import SubmissionController
from intranet.femida.src.candidates.submissions.helpers import (
    get_applications_waiting_contest_results,
)
from intranet.femida.src.candidates.helpers import blank_modify_candidate
from intranet.femida.src.candidates.tasks import create_onedayoffer_issue_task
from intranet.femida.src.contest.api import ContestConflictError
from intranet.femida.src.contest.helpers import (
    get_contest_url,
    get_contest_results_by_participants,
    register_to_contest,
    register_to_contest_by_answer_id,
    get_participant_ids_by_answer_ids,
)
from intranet.femida.src.core.signals import post_update
from intranet.femida.src.interviews.choices import APPLICATION_RESOLUTIONS
from intranet.femida.src.notifications.applications import ApplicationResolutionChangedNotification
from intranet.femida.src.notifications.contest import (
    notify_about_failed_registration_to_contest,
    notify_about_successful_registration_to_contest,
)
from intranet.femida.src.users.models import get_robot_femida
from intranet.femida.src.utils.itertools import get_chunks
from intranet.femida.src.utils.strings import fetch_comma_separated_integers


logger = logging.getLogger(__name__)


@transaction.atomic
@action_logged('internship_submission_handle')
def handle_internship_submission(submission):
    """
    Обработка отклика на стажировку.
    Создаем нового кандидата или мерджим с существующим.
    Регистрируем кандидата на контест.
    Отправляем письмо про регистрацию на контест.
    """
    controller = SubmissionController(submission)

    if not controller.vacancies.exists():
        logger.warning(
            'Submission `%d` does not have active vacancies. '
            'Submission handle will be ignored',
            submission.id
        )
        return

    duplicates = list(deduplication.DuplicatesFinder().find(
        candidate=submission,
        strategy=deduplication.strategies.new_strategy,
    ))
    if duplicates:
        # Берем дубликат с наибольшей уверенностью дублирования и максимальным score
        duplicate, similarity, decision = max(duplicates, key=lambda x: (x[2], x[1]))
        if decision == deduplication.DEFINITELY_DUPLICATE:
            candidate = controller.merge_into_candidate(candidate=duplicate)
        else:
            candidate = controller.create_candidate()
            DuplicationCase.objects.create(
                first_candidate=candidate,
                second_candidate=duplicate,
                is_active=True,
                score=similarity.score,
                is_auto_merge=False,
            )
    else:
        candidate = controller.create_candidate()

    # Если будет ContestConflictError, то мы просто не создадим challenge, но кандидата дообработаем
    # Если же будет другая ошибка (например таймаут),
    # то транзакцию откатим и celery-таску перезапустим
    link_candidate_to_contest_by_submission(
        submission=submission,
        consideration=controller.consideration,
    )
    return candidate


def link_candidate_to_contest_by_submission(submission, consideration):
    """
    Зарегистрировать кандидата на контест в Контесте и создать черновой Challenge
    """
    contest_id = submission.contest_id
    login = submission.login
    is_notification_required = submission.is_contest_notification_required
    passcode = submission.passcode
    answer_id = submission.answer_id
    participant_id = None
    is_onedayoffer = submission.is_onedayoffer

    if is_onedayoffer:
        register_to_contest_by_answer_id(
            contest_id=contest_id,
            answer_id=answer_id,
            passcode=passcode,
        )
    else:
        try:
            participant_id = register_to_contest(contest_id, login)
        except ContestConflictError:
            logger.warning(
                'Candidate `%d` with login `%s` is already registered to contest `%d`',
                submission.candidate_id, login, contest_id,
            )
            if is_notification_required:
                notify_about_failed_registration_to_contest(submission)
            return

    challenge = Challenge.objects.create(
        type=choices.CHALLENGE_TYPES.contest,
        status=choices.CHALLENGE_STATUSES.assigned,
        candidate_id=submission.candidate_id,
        consideration=consideration,
        submission=submission,
        answers={
            'contest': {
                'id': contest_id,
                # TODO: Ни к чему хранить url в базе
                'url': get_contest_url(contest_id),
            },
            'participation': {
                'id': participant_id,
            },
        }
    )
    update_consideration_extended_status(consideration)
    blank_modify_candidate(consideration.candidate)
    if is_notification_required:
        notify_about_successful_registration_to_contest(submission)
    if is_onedayoffer:
        create_onedayoffer_issue_task.delay(challenge.id)


def finish_onedayoffer_challenge(contest_id, results, instance, user):
    threshold_by_contest_id = json.loads(config.ONEDAYOFFER_THRESHOLD_BY_CONTEST_ID)
    threshold = threshold_by_contest_id.get(str(contest_id), config.DEFAULT_ONEDAYOFFER_THRESHOLD)
    try:
        score = float(results['results']['score'])
    except (ValueError, TypeError):
        score = results['results']['score']
        logger.warning('Invalid score for contest %s: %s', contest_id, score)
        return
    if score >= float(threshold):
        resolution = choices.CHALLENGE_RESOLUTIONS.hire
    else:
        resolution = choices.CHALLENGE_RESOLUTIONS.nohire

    workflow = ChallengeWorkflow(instance=instance, user=user)
    action = workflow.get_action('estimate')
    action.perform(
        comment='',
        resolution=resolution,
        strict=False,
    )


def sync_contest_results(contest_id, challenges_by_participant_id):
    """
    :param contest_id: ID контеста, который синкаем
    :param challenges_by_participant_id: challenges, сгруппированные по participant_id
    :type challenges_by_participant_id: dict
    :return:
    """
    contest_results = get_contest_results_by_participants(
        contest_id=contest_id,
        participant_ids=set(challenges_by_participant_id),
    )
    logger.info(
        '%d of %d participations were loaded for contest_id `%d`',
        len(contest_results),
        len(challenges_by_participant_id),
        contest_id,
    )
    submission_ids = []
    robot_femida = get_robot_femida()
    for participant_id, results in contest_results.items():
        challenge = challenges_by_participant_id[participant_id]
        submission_ids.append(challenge.submission_id)
        challenge.answers = results

        if challenge.submission.is_onedayoffer and challenge.submission.autoestimate:
            finish_onedayoffer_challenge(
                contest_id=contest_id,
                results=results,
                instance=challenge,
                user=robot_femida,
            )
            continue

        challenge.status = choices.CHALLENGE_STATUSES.pending_review
        challenge.save()
        update_consideration_extended_status(challenge.consideration)

    # Меняем резолюцию у соответствующих претенденств
    applications = get_applications_waiting_contest_results(submission_ids)
    application_ids = [a.id for a in applications]
    applications.update(resolution=APPLICATION_RESOLUTIONS.test_task_done)
    updated_applications = Application.unsafe.filter(id__in=application_ids)
    post_update.send(
        sender=Application,
        queryset=updated_applications,
    )
    for application in updated_applications:
        notification = ApplicationResolutionChangedNotification(application, robot_femida)
        notification.send()


def get_challenges_by_contest_id(full=False):
    contest_blacklist = fetch_comma_separated_integers(config.CONTEST_BLACKLIST)
    statuses = [choices.CHALLENGE_STATUSES.assigned]
    if full:
        statuses.append(choices.CHALLENGE_STATUSES.pending_review)

    challenges = (
        Challenge.objects
        .filter(
            type=choices.CHALLENGE_TYPES.contest,
            status__in=statuses,
            consideration__state=choices.CONSIDERATION_STATUSES.in_progress,
        )
        .exclude(answers__participation__id=None)
        .select_related(
            'submission',
            'consideration',
        )
    )
    challenges_by_contest_id = defaultdict(dict)
    for challenge in challenges:
        contest_id = challenge.answers['contest']['id']
        if contest_id in contest_blacklist:
            continue
        participant_id = challenge.answers['participation']['id']
        challenges_by_contest_id[contest_id][participant_id] = challenge
    return challenges_by_contest_id


@transaction.atomic
@action_logged('sync_contests_results')
def sync_actual_contests_results(full=False):
    """
    Для каждого незакрытого Challenge типа contest проверяем закончилось ли соревнование.
    Обновляем данные.
    """
    challenges_by_contest_id = get_challenges_by_contest_id(full)
    logger.info('Start to sync %d contests.', len(challenges_by_contest_id))
    for contest_id in challenges_by_contest_id:
        sync_contest_results(
            contest_id=contest_id,
            challenges_by_participant_id=challenges_by_contest_id[contest_id],
        )


@transaction.atomic
@action_logged('sync_contest_participant_ids')
def sync_contest_participant_ids():
    challenges = (
        Challenge.objects
        .filter(
            type=choices.CHALLENGE_TYPES.contest,
            status=choices.CHALLENGE_STATUSES.assigned,
            consideration__state=choices.CONSIDERATION_STATUSES.in_progress,
            answers__participation__id=None,
        )
        .select_related('submission')
    )

    if not challenges:
        return

    challenge_by_answer_id = {c.submission.answer_id: c for c in challenges}
    all_answer_ids = list(challenge_by_answer_id)
    challenges_to_update = []

    for answer_ids in get_chunks(all_answer_ids, 100):
        data = get_participant_ids_by_answer_ids(answer_ids)
        for item in data['result']['searchResults']:
            if item['status'] == 'ok':
                challenge = challenge_by_answer_id[item['answerId']]
                challenge.answers['participation']['id'] = item['participantId']
                challenges_to_update.append(challenge)
            elif item['status'] != 'not-registered':
                logger.warning(
                    'Failed to get contest participant_id by answer_id: %s, status: %s, error: %s',
                    item['answerId'],
                    item['status'],
                    item.get('error'),
                )

    Challenge.objects.bulk_update(challenges_to_update, fields=['answers'], batch_size=100)
    post_update.send(sender=Challenge, queryset=challenges_to_update)
