import bisect
import json
from datetime import timedelta
from logging import getLogger

from constance import config
from django.db.models import OuterRef, Prefetch
from django.utils import timezone
from django.utils.dateparse import parse_datetime

from intranet.femida.src.core.db import RowsToList
from intranet.femida.src.core.db.helpers import get_count_subquery
from intranet.femida.src.interviews.choices import APPLICATION_SOURCES, APPLICATION_STATUSES
from intranet.femida.src.interviews.models import Application
from intranet.femida.src.staff.models import Department
from intranet.femida.src.vacancies.choices import VACANCY_STATUSES, VACANCY_PRO_LEVELS
from intranet.femida.src.vacancies.models import VacancyHistory


logger = getLogger(__name__)


def rank_proposals(applications, formula=None):
    """
    Функция ранжирования вакансий в рамках массового предложения.
    """
    formula = formula or config.PROPOSALS_RANKING_FORMULA
    try:
        weights = json.loads(formula)
        if not isinstance(weights, dict):
            raise ValueError
    except ValueError:
        logger.warning('Cannot parse formula: %s', formula)
        return applications

    try:
        for application in applications:
            application.proposal_factors['relevance'] = _get_relevance(application, weights)
        applications = sorted(applications, key=lambda a: -a.proposal_factors['relevance'])
    except Exception:
        logger.exception('Got an error while trying to rank proposals')
        return applications

    return applications[:config.PROPOSALS_RANKING_LIMIT]


def _get_relevance(application, weights):
    relevance = 0
    for name, weight in weights.items():
        relevance += weight * application.proposal_factors.get(name, 0)
    return relevance


class FactorBase:
    """
    Базовый класс для факторов ранжирования вакансий при массовом предложении нанимающим

    Факторы используются для вычисления формулы ранжирования
    Смысл формул подсчета факторов см. в https://st.yandex-team.ru/FEMIDA-3859
    """
    name = None

    def __init__(self):
        self.annotation_name = '{}_annotation'.format(self.name)

    def annotate_qs(self, qs):
        """
        Аннотировать внешний qs по всем вакансиям данными, необходимыми для вычисления фактора
        """
        return qs

    def compute_factor(self, vacancy, filter_params):
        """
        Вычислить значение фактора для заданной вакансии.
        :param vacancy: Vacancy, аннотированная полем self.annotation_name
        """
        raise NotImplementedError

    def _annotate_with(self, qs, annotation):
        return qs.annotate(
            **{
                self.annotation_name: annotation,
            }
        )


class InvertProposalFrequencyFactor(FactorBase):
    """
    Формула: 1 / (1 + количество предложенных кандидатов за 30 дней)
    """
    name = 'invert_proposal_frequency'

    def annotate_qs(self, qs):
        now = timezone.now()
        return self._annotate_with(
            qs=qs,
            annotation=get_count_subquery(
                queryset=(
                    Application.unsafe
                    .filter(
                        source=APPLICATION_SOURCES.proposal,
                        created__range=(now - timedelta(days=30), now),
                    )
                ),
                reverse_related_name='vacancy',
            )
        )

    def compute_factor(self, vacancy, filter_params):
        n_candidates_proposed = getattr(vacancy, self.annotation_name)
        return 1.0 / (1 + n_candidates_proposed)


class InvertActiveApplicationQuantityV2Factor(FactorBase):
    """
    Чем меньше активных прет-в на вакансии, тем выше фактор
    - 1, если ≤ 5 прет-в
    - 0.5, если > 5, но ≤ 9
    - 0, если > 9
    """
    name = 'invert_active_application_quantity_v2'
    _thresholds = [5, 9]
    _values = [1.0, 0.5, 0.0]

    def annotate_qs(self, qs):
        return self._annotate_with(
            qs=qs,
            annotation=get_count_subquery(
                queryset=(
                    Application.unsafe
                    .filter(status=APPLICATION_STATUSES.in_progress)
                ),
                reverse_related_name='vacancy',
            )
        )

    def compute_factor(self, vacancy, filter_params):
        active_applications_count = getattr(vacancy, self.annotation_name)
        index = bisect.bisect_left(self._thresholds, active_applications_count)
        return self._values[index]


class VacancyDurationV2Factor(FactorBase):
    """
    Фактор длительности вакансии.
    Чем дольше вакансия в работе, тем выше её фактор длительности:
    - 0, если ≤ 90 дней
    - 0.5, если > 90, но ≤ 180 дней
    - 1, если > 180 дней
    """
    name = 'vacancy_duration_v2'
    _thresholds = [90, 180]
    _values = [0.0, 0.5, 1.0]

    def annotate_qs(self, qs):
        annotation_qs = (
            VacancyHistory.objects
            .filter(vacancy=OuterRef('id'))
            .order_by('changed_at')
            .values(
                'status',
                'changed_at',
            )
        )
        return self._annotate_with(
            qs=qs,
            annotation=RowsToList(
                annotation_qs,
            ),
        )

    def compute_factor(self, vacancy, filter_params):
        vacancy_history = getattr(vacancy, self.annotation_name) or []
        days_in_progress = self._compute_duration(vacancy_history)
        index = bisect.bisect_left(self._thresholds, days_in_progress)
        return self._values[index]

    def _compute_duration(self, vacancy_history):
        duration = timedelta()
        start_time = None
        for history_elem in vacancy_history:
            changed_at = parse_datetime(history_elem['changed_at'])
            if history_elem['status'] == VACANCY_STATUSES.in_progress:
                # Игнорируем подряд идущие активные статусы
                if start_time is not None:
                    continue
                start_time = changed_at
            elif start_time is not None:
                duration += changed_at - start_time
                start_time = None
        # Если вакансия активна в настоящее время
        if start_time is not None:
            duration += timezone.now() - start_time
        return duration.days


class CandidateVacancySkillsIntersectionFactor(FactorBase):

    name = 'skills_intersection'

    def annotate_qs(self, qs):
        return qs.prefetch_related('skills')

    def compute_factor(self, vacancy, filter_params):
        vacancy_skills = set(s.id for s in vacancy.skills.all())
        if not vacancy_skills:
            return 0.0

        expected_skills = set(s.id for s in filter_params.get('skills', []))
        return float(len(vacancy_skills & expected_skills)) / len(vacancy_skills)


class ProfessionsIntersectionFactor(FactorBase):

    name = 'professions_intersection'

    def compute_factor(self, vacancy, filter_params):
        if not vacancy.profession_id:
            return 0.0

        expected_professions = set(p.id for p in filter_params.get('professions', []))
        return 1.0 if vacancy.profession_id in expected_professions else 0.0


class CitiesIntersectionFactor(FactorBase):

    name = 'cities_intersection'

    def annotate_qs(self, qs):
        return qs.prefetch_related('cities')

    def compute_factor(self, vacancy, filter_params):
        vacancy_cities = set(city.id for city in vacancy.cities.all())
        expected_cities = set(city.id for city in filter_params.get('cities', []))
        return 1.0 if vacancy_cities & expected_cities else 0.0


class ProLevelsIntersectionV2Factor(FactorBase):
    """
    Формула: кол-во пересекаемых уровней / кол-во уровней на вакансии
    """
    name = 'pro_levels_intersection_v2'

    def compute_factor(self, vacancy, filter_params):
        vacancy_level_min = vacancy.pro_level_min or VACANCY_PRO_LEVELS.intern
        vacancy_level_max = vacancy.pro_level_max or VACANCY_PRO_LEVELS.expert

        filter_level_min = filter_params.get('pro_level_min') or VACANCY_PRO_LEVELS.intern
        filter_level_max = filter_params.get('pro_level_max') or VACANCY_PRO_LEVELS.expert

        common_level_min = max(vacancy_level_min, filter_level_min)
        common_level_max = min(vacancy_level_max, filter_level_max)

        common_levels_count = common_level_max - common_level_min + 1
        vacancy_levels_count = vacancy_level_max - vacancy_level_min + 1

        if common_levels_count < 1:
            return 0.0

        return common_levels_count / float(vacancy_levels_count)


class InterviewByDepartmentFactor(FactorBase):
    """
    Фактор, означающий, что в текущем рассмотрении кандидата
    отделом-направлением, к которому относится вакансия,
    проводились секции. Если да – 1.0, нет – 0.0
    """
    name = 'interview_by_department'

    def annotate_qs(self, qs):
        return qs.prefetch_related(Prefetch(
            lookup='department',
            queryset=Department.objects.with_direction(),
        ))

    def compute_factor(self, vacancy, filter_params):
        if not vacancy.department:
            return 0.0
        direction_ids = filter_params.get('interview_direction_ids', set())
        return float(vacancy.department.direction_id in direction_ids)


VACANCY_FACTORS = (
    InvertProposalFrequencyFactor(),
    InvertActiveApplicationQuantityV2Factor(),
    VacancyDurationV2Factor(),
    CandidateVacancySkillsIntersectionFactor(),
    ProfessionsIntersectionFactor(),
    CitiesIntersectionFactor(),
    ProLevelsIntersectionV2Factor(),
    InterviewByDepartmentFactor(),
)


def get_factors(vacancy, filter_params):
    filter_params = filter_params or {}
    factors = {
        factor.name: factor.compute_factor(vacancy, filter_params)
        for factor in VACANCY_FACTORS
    }
    return factors
