from datetime import date
from itertools import groupby
import logging
from typing import Dict, List, Tuple, Set

import attr

from staff.budget_position.workflow_service.entities.interfaces import (
    GradeData,
    ReviewSchemeIdRequest,
    ReviewSchemeIdByGroupRequest,
    BonusSchemeIdRequest,
    BonusSchemeIdByGroupRequest,
    RewardCategoryRequest,
    RewardSchemeIdRequest,
)
from staff.budget_position.workflow_service.entities.service_dtos import OccupationDetails
from staff.budget_position.workflow_service.entities.types import OccupationId, BudgetPositionCode, PersonId


logger = logging.getLogger(__name__)


@attr.s(auto_attribs=True)
class SchemesRequestParams:
    """
    Описывает параметры запроса для каждого отдельно взятого ChangeSchemeRequest
    Используется для дальнейшего запроса схем в tableflow
    """
    occupation_id: OccupationId
    grade_level: int
    department_id: int
    is_internship: bool


@attr.s(auto_attribs=True, frozen=True)
class ChangeSchemeRequest:
    """
    Описывает каждое запрошиваемое изменение, на основании этих параметров мы решим надо ли запрашивать
    схемы в tableflow и что конкретно мы должны запрашивать
    """
    person_id: int
    budget_position: BudgetPositionCode or None
    occupation_id: OccupationId or None
    grade_level: int or None
    department_id: int or None
    is_internship: bool or None
    oebs_date: date
    force_recalculate_schemes: bool or None


class SchemeRequestsCollector:
    def __init__(self, requests: List[ChangeSchemeRequest]):
        self._change_scheme_requests = requests
        self._requests: Dict[ChangeSchemeRequest, SchemesRequestParams] = {}
        self._involved_occupations: Set[OccupationId] = set()
        self._review_scheme_ids_to_request_by_group: Set[ChangeSchemeRequest] = set()
        self._bonus_scheme_ids_to_request_by_group: Set[ChangeSchemeRequest] = set()

    def review_scheme_id_not_found_by_occupation(self, change_scheme_request: ChangeSchemeRequest):
        """
        Метод нужен чтобы уведомить этот класс, что поиск схем по конкретной occupation не дал результатов
        а значит, это ChangeSchemeRequest не удовлетворен, и мы должны еще поискать по группе occupation
        """
        self._review_scheme_ids_to_request_by_group.add(change_scheme_request)

    def review_scheme_id_requests_by_occupation(self) -> List[Tuple[ChangeSchemeRequest, ReviewSchemeIdRequest]]:
        result = []
        for change_scheme_request, params in self._requests.items():
            review_scheme_id_request = ReviewSchemeIdRequest(
                params.occupation_id,
                params.department_id,
                params.grade_level,
            )
            result.append((change_scheme_request, review_scheme_id_request))

        return result

    def review_scheme_id_requests_by_occupation_groups(
        self,
        occupation_groups: Dict[OccupationId, OccupationDetails or None],
    ) -> List[Tuple[ChangeSchemeRequest, ReviewSchemeIdByGroupRequest]]:
        result = []
        for change_scheme_request, params in self._requests.items():
            if change_scheme_request in self._review_scheme_ids_to_request_by_group:
                occupation_group = occupation_groups.get(params.occupation_id)
                if occupation_group:
                    request = ReviewSchemeIdByGroupRequest(
                        occupation_review_group=occupation_group.review_group,
                        department_id=params.department_id,
                        grade_level=params.grade_level,
                    )
                    result.append((change_scheme_request, request))
        return result

    def bonus_scheme_id_not_found_by_occupation(self, change_scheme_request: ChangeSchemeRequest):
        """
        Метод нужен чтобы уведомить этот класс, что поиск схем по конкретной occupation не дал результатов
        а значит, это ChangeSchemeRequest не удовлетворен, и мы должны еще поискать по группе occupation
        """
        self._bonus_scheme_ids_to_request_by_group.add(change_scheme_request)

    def bonus_scheme_id_requests_by_occupation(self) -> List[Tuple[ChangeSchemeRequest, BonusSchemeIdRequest]]:
        """
        На каждый запрос на изменение схемы (ChangeSchemeRequest) возвращает запрос для tableflow для выяснения схемы
        по Occupation
        """
        result = []
        for change_scheme_request, params in self._requests.items():
            bonus_scheme_id_request = BonusSchemeIdRequest(
                department_id=params.department_id,
                occupation_id=params.occupation_id,
                grade_level=params.grade_level,
            )

            result.append((change_scheme_request, bonus_scheme_id_request))
        return result

    def bonus_scheme_id_requests_by_occupation_groups(
        self,
        occupation_groups: Dict[OccupationId, OccupationDetails or None],
    ) -> List[Tuple[ChangeSchemeRequest, BonusSchemeIdByGroupRequest]]:
        result = []
        for change_scheme_request, params in self._requests.items():
            if change_scheme_request in self._bonus_scheme_ids_to_request_by_group:
                occupation_group = occupation_groups.get(params.occupation_id)
                if occupation_group:
                    bonus_scheme_id_by_group_request = BonusSchemeIdByGroupRequest(
                        occupation_bonus_group=occupation_group.bonus_group,
                        department_id=params.department_id,
                        grade_level=params.grade_level,
                    )
                    result.append((change_scheme_request, bonus_scheme_id_by_group_request))
        return result

    def reward_category_requests_by_occupation_group(
        self,
        occupation_groups: Dict[OccupationId, OccupationDetails or None],
    ) -> List[Tuple[ChangeSchemeRequest, RewardCategoryRequest]]:
        """
        Генерирует запросы в tableflow на поиск категории, для каждого ChangeSchemeRequest
        """
        result = []
        for change_scheme_request, params in self._requests.items():
            occupation_group = occupation_groups.get(params.occupation_id)
            if occupation_group:
                reward_category_request = RewardCategoryRequest(
                    occupation_reward_group=occupation_group.reward_group,
                    grade_level=params.grade_level,
                )
                result.append((change_scheme_request, reward_category_request))

        return result

    def reward_scheme_id_requests_by_category(
        self,
        reward_categories: Dict[ChangeSchemeRequest, str],
    ) -> List[Tuple[ChangeSchemeRequest, RewardSchemeIdRequest]]:
        result = []
        for change_scheme_request, reward_category in reward_categories.items():
            assert reward_category
            scheme_request_params = self._requests[change_scheme_request]
            reward_scheme_id_request = RewardSchemeIdRequest(
                department_id=scheme_request_params.department_id,
                category=reward_category,
                is_internship=scheme_request_params.is_internship,
            )
            result.append((change_scheme_request, reward_scheme_id_request))
        return result

    def has_any_change_that_leads_to_scheme_recalculation(self) -> bool:
        return bool(self._change_scheme_requests_that_leads_to_scheme_recalculation())

    def _change_scheme_requests_that_leads_to_scheme_recalculation(self) -> List[ChangeSchemeRequest]:
        return list(
            change_scheme_request
            for change_scheme_request in self._change_scheme_requests
            if self._request_leads_to_scheme_recalculation(change_scheme_request)
        )

    def involved_staff_ids(self) -> List[PersonId]:
        return [
            single_data.person_id
            for single_data in filter(self._request_leads_to_scheme_recalculation, self._change_scheme_requests)
            if single_data.person_id is not None
        ]

    def exclude_persons_from_further_calculations(self, person_ids: List[PersonId]) -> None:
        person_ids = set(person_ids)
        self._change_scheme_requests = [
            request
            for request in self._change_scheme_requests
            if request.person_id not in person_ids
        ]

    @property
    def involved_occupations(self) -> Set[OccupationId]:
        return self._involved_occupations

    def _request_leads_to_scheme_recalculation(self, change_scheme_request: ChangeSchemeRequest) -> bool:
        return all((
            change_scheme_request.person_id is not None,
            change_scheme_request.budget_position,
            any((
                change_scheme_request.force_recalculate_schemes,
                change_scheme_request.occupation_id is not None,
                change_scheme_request.department_id is not None,
                change_scheme_request.grade_level is not None,
            )),
        ))

    def prepare_data_for_request_by_occupation(
        self,
        person_departments: Dict[int, int],
        person_grades: Dict[int, GradeData]
    ):
        assert self.has_any_change_that_leads_to_scheme_recalculation()
        self._requests = {}
        self._involved_occupations = set()
        change_scheme_requests = sorted(
            self._change_scheme_requests_that_leads_to_scheme_recalculation(),
            key=lambda request: request.person_id,
        )
        for person_id, requests_by_person in groupby(change_scheme_requests, key=lambda request: request.person_id):
            department_id = person_departments.get(person_id)
            if not department_id:
                logger.error('Department not found for person %s', person_id)
                continue

            grade_data = person_grades.get(person_id)
            if not grade_data:
                logger.info('Grade not found for person %s', person_id)
                continue

            self._collect_for_single_person(list(requests_by_person), department_id, grade_data)

    def _collect_for_single_person(
        self,
        requests: List[ChangeSchemeRequest],
        department_id: int,
        grade_data: GradeData,
    ):
        requests = [request for request in requests if self._request_leads_to_scheme_recalculation(request)]
        requests_by_date = sorted(requests, key=lambda request: request.oebs_date)

        department_id_for_current_change = department_id
        grade_level_for_current_change = grade_data.level
        occupation_for_current_change = grade_data.occupation

        for change_scheme_request in requests_by_date:
            if change_scheme_request.department_id:
                department_id_for_current_change = change_scheme_request.department_id

            if change_scheme_request.grade_level:
                grade_level_for_current_change = change_scheme_request.grade_level

            if change_scheme_request.occupation_id:
                occupation_for_current_change = change_scheme_request.occupation_id

            is_internship = bool(change_scheme_request.is_internship)

            self._involved_occupations.add(occupation_for_current_change)

            scheme_request_params = SchemesRequestParams(
                occupation_id=occupation_for_current_change,
                grade_level=grade_level_for_current_change,
                department_id=department_id_for_current_change,
                is_internship=is_internship,
            )

            self._add_scheme_requests_param(change_scheme_request, scheme_request_params)

    def _add_scheme_requests_param(self, scheme_change_request: ChangeSchemeRequest, params: SchemesRequestParams):
        self._requests[scheme_change_request] = params
