import logging
from typing import Dict, List, Optional, Set
from uuid import UUID

from staff.lib import waffle
from staff.lib.log import log_context

from staff.budget_position.workflow_service import entities, gateways, OEBSError


logger = logging.getLogger(__name__)


class CreateWorkflowFromProposal:
    def __init__(
        self,
        repository: entities.WorkflowRepositoryInterface,
        budget_positions_repo: entities.BudgetPositionsRepository,
        staff_service: gateways.StaffService,
        grade_calculator: entities.GradeCalculator,
        schemes_calculator: entities.SchemesCalculator,
        femida_service: entities.FemidaService,
        proposal_workflow_factory: entities.AbstractProposalWorkflowFactory,
    ):
        self._repository = repository
        self._budget_positions_repo = budget_positions_repo
        self._staff_service = staff_service
        self._grade_calculator = grade_calculator
        self._schemes_calculator = schemes_calculator
        self._femida_service = femida_service
        self._proposal_workflow_factory = proposal_workflow_factory

    def create(self, data: List[entities.ProposalData]) -> (List[UUID], List[entities.AbstractWorkflow]):
        created_workflow_uuids: List[UUID] = []
        conflicting_workflows: List[entities.AbstractWorkflow] = []

        budget_positions_calculator = entities.BudgetPositionsCalculator(
            self._budget_positions_repo,
            self._femida_service,
            data,
        )

        if waffle.switch_is_active('enable_oebs_requests_for_registry'):
            persons_budget_positions = budget_positions_calculator.budget_positions_for_person_changes()
            persons_who_has_budget_positions = {
                person_id
                for person_id, budget_position_move in persons_budget_positions.items()
                if budget_position_move.has_target_budget_position
            }
            self._fill_grades(data, persons_who_has_budget_positions)
            self._find_schemes(data, persons_budget_positions)

        for proposal_data in data:
            with log_context(proposal_id=proposal_data.proposal_id, expected_exceptions=[OEBSError]):
                try:
                    logger.info('Creating workflow')
                    budget_position_move = budget_positions_calculator.budget_position_move(proposal_data)

                    if budget_position_move.has_target_budget_position:
                        self._check_for_conflicts(budget_position_move.new_budget_position.code)

                    workflow = self._proposal_workflow_factory.create_workflow(
                        proposal_data,
                        budget_position_move,
                    )
                    workflow.proposal_id = proposal_data.proposal_id
                    workflow.vacancy_id = proposal_data.vacancy_id

                    logger.info('Created workflow %s with id %s', workflow.code, workflow.id)
                    self._repository.save(workflow)
                    created_workflow_uuids.append(workflow.id)
                except entities.ConflictWorkflowExistsError as e:
                    conflicting_workflows += e.conflicting_workflows
        return created_workflow_uuids, conflicting_workflows

    def _find_schemes(
        self,
        proposal_data_list: List[entities.ProposalData],
        persons_headcount_positions: entities.BudgetPositionCalculationResults,
    ):
        if not waffle.switch_is_active('enable_table_flow_for_registry'):
            return

        request_to_requested_change = {}

        for proposal_data in proposal_data_list:
            if proposal_data.person_id:
                budget_position_move = persons_headcount_positions.get(proposal_data.person_id)
                if not budget_position_move.has_target_budget_position:
                    logger.info('Budget position for person %s not found, will not request schemes')
                    continue

                for requested_change in proposal_data.proposal_changes:
                    change_scheme_request = entities.ChangeSchemeRequest(
                        person_id=proposal_data.person_id,
                        budget_position=budget_position_move.new_budget_position.code,
                        occupation_id=requested_change.occupation_id,
                        grade_level=requested_change.grade_level,
                        department_id=requested_change.department_id,
                        is_internship=requested_change.is_internship,
                        oebs_date=requested_change.oebs_date,
                        force_recalculate_schemes=requested_change.force_recalculate_schemes,
                    )
                    request_to_requested_change[change_scheme_request] = requested_change

        result = self._schemes_calculator.schemes_set_for_mass_person_changes(
            list(request_to_requested_change.keys())
        )
        for change_scheme_request, schemes_set in result.items():
            proposal_change = request_to_requested_change[change_scheme_request]
            proposal_change.review_scheme_id = schemes_set.review_scheme_id
            proposal_change.bonus_scheme_id = schemes_set.bonus_scheme_id
            proposal_change.reward_scheme_id = schemes_set.reward_scheme_id

    def _grade_changes(
        self,
        data: List[entities.ProposalData],
        persons_with_budget_positions: Set[int],
    ) -> Dict[int, List[entities.GradeChange]]:
        result = {}

        data_eligible_for_grade_changes = [
            person_data
            for person_data in data
            if person_data.has_person_id and person_data.person_id in persons_with_budget_positions
        ]
        data_eligible_for_grade_changes = [
            person_data
            for person_data in data_eligible_for_grade_changes
            if person_data.has_any_grade_or_department_or_occupation_change
        ]

        for person_data in data_eligible_for_grade_changes:
            person_grade_changes = [
                entities.GradeChange(
                    new_occupation_id=change.occupation_id,
                    grade_level_change=int(change.grade_change) if change.grade_change else None,
                )
                for change in person_data.proposal_changes
            ]
            result[person_data.person_id] = person_grade_changes

        return result

    def _person_data_by_staff_id(self, data: List[entities.ProposalData]) -> Dict[int, entities.ProposalData]:
        return {person_data.person_id: person_data for person_data in data if person_data.person_id}

    def _fill_grades(
        self,
        data: List[entities.ProposalData],
        persons_with_budget_positions: Set[int],
    ):
        """
        Проставляет grade_id и grade_level в PersonChange если это необходимо
        """
        person_data_by_staff_id = self._person_data_by_staff_id(data)
        grade_changes = self._grade_changes(data, persons_with_budget_positions)

        if not grade_changes:
            return

        calculated_grades = self._grade_calculator.calculate_new_grades(grade_changes)

        for person_id, person_grade_changes in calculated_grades.items():
            person_data = person_data_by_staff_id[person_id]
            for idx, person_grade_change in enumerate(person_grade_changes):
                if person_grade_change:
                    change = person_data.proposal_changes[idx]
                    change.grade_id, change.grade_level = person_grade_change.grade_id, person_grade_change.grade_level

    def _check_for_conflicts(self, budget_position_code: Optional[int]) -> None:
        if waffle.switch_is_active('ignore_budget_position_conflicts'):
            return

        if budget_position_code is None:
            return

        conflicting_workflows = self._repository.get_pending_workflows_by_budget_position_code(budget_position_code)

        if conflicting_workflows:
            logger.info('Found conflicting workflow %s', [workflow.id for workflow in conflicting_workflows])
            raise entities.ConflictWorkflowExistsError(conflicting_workflows)
