import logging
from typing import Dict, List, Optional

import attr

from staff.budget_position.workflow_service.entities.budget_positon import BudgetPosition
from staff.budget_position.workflow_service.entities.interfaces import BudgetPositionsRepository, FemidaService
from staff.budget_position.workflow_service.entities.service_dtos import ProposalData


logger = logging.getLogger(__name__)


@attr.s(auto_attribs=True)
class BudgetPositionMove:
    old_budget_position: Optional[BudgetPosition]
    new_budget_position: Optional[BudgetPosition]

    @property
    def has_any_budget_position(self) -> bool:
        return bool(self.old_budget_position) or bool(self.new_budget_position)

    @property
    def has_target_budget_position(self) -> bool:
        return bool(self.new_budget_position)

    @property
    def budget_position_will_be_changed(self) -> bool:
        if self.old_budget_position and self.new_budget_position:
            return self.old_budget_position != self.new_budget_position

        return False


BudgetPositionCalculationResults = Dict[int, BudgetPositionMove]


class BudgetPositionsCalculator:
    def __init__(
        self,
        budget_positions_repo: BudgetPositionsRepository,
        femida_service: FemidaService,
        proposal_data_list: List[ProposalData],
    ) -> None:
        self._proposal_data_list = proposal_data_list
        self._budget_positions_repo = budget_positions_repo
        self._femida_service = femida_service

        self._budget_positions_for_person_changes_cache: Optional[BudgetPositionCalculationResults] = None
        self._budget_positions_for_vacancy_changes_cache: Optional[BudgetPositionCalculationResults] = None
        self._budget_positions_for_free_headcount_changes_cache: Optional[BudgetPositionCalculationResults] = None

    def _budget_positions_for_changes_without_moving_budget_position(self) -> BudgetPositionCalculationResults:
        person_ids_with_move_without_budget_positions = {
            proposal_data.person_id: proposal_data.job_ticket_url
            for proposal_data in self._proposal_data_list
            if (
                proposal_data.has_person_id
                and not proposal_data.is_move_with_budget_position
                and proposal_data.job_ticket_url
            )
        }

        if not person_ids_with_move_without_budget_positions:
            return {}

        job_ticket_urls = list(person_ids_with_move_without_budget_positions.values())
        logger.info('Will search vacancies for urls %s', job_ticket_urls)
        found_vacancies = self._femida_service.vacancies_by_job_ticket_urls(job_ticket_urls=job_ticket_urls)

        if not found_vacancies:
            return {}

        found_budget_positions = self._budget_positions_repo.headcount_position_data_by_vacancy_ids(
            vacancy_ids=list(found_vacancies.values())
        )

        result = {}
        for person_id, job_ticket in person_ids_with_move_without_budget_positions.items():
            vacancy_id = found_vacancies.get(job_ticket)
            if vacancy_id:
                budget_position = found_budget_positions[vacancy_id]
                if budget_position:
                    result[person_id] = BudgetPositionMove(None, budget_position)
                else:
                    logger.warning('Budget position not found for vacancy %s', vacancy_id)
            else:
                logger.warning('Vacancy id not found for ticket %s', job_ticket)

        return result

    def budget_positions_for_person_changes(self) -> BudgetPositionCalculationResults:
        if self._budget_positions_for_person_changes_cache is not None:
            return self._budget_positions_for_person_changes_cache

        persons_budget_positions = self._budget_positions_repo.headcount_position_data_by_persons_ids([
            proposal_data.person_id
            for proposal_data in self._proposal_data_list
            if proposal_data.has_person_id
        ])

        result = {
            person_id: BudgetPositionMove(budget_position, budget_position)
            for person_id, budget_position in persons_budget_positions.items()
        }

        new_budget_positions_for_persons = self._budget_positions_for_changes_without_moving_budget_position()
        for person_id, new_budget_position in new_budget_positions_for_persons.items():
            result[person_id] = BudgetPositionMove(
                old_budget_position=result[person_id].old_budget_position,
                new_budget_position=new_budget_position.new_budget_position,
            )

        self._budget_positions_for_person_changes_cache = result
        return self._budget_positions_for_person_changes_cache

    def _budget_positions_for_vacancy_changes(self) -> BudgetPositionCalculationResults:
        if self._budget_positions_for_vacancy_changes_cache is not None:
            return self._budget_positions_for_vacancy_changes_cache

        vacancy_headcount_positions = self._budget_positions_repo.headcount_position_data_by_vacancy_ids([
            proposal_data.vacancy_id
            for proposal_data in self._proposal_data_list
            if proposal_data.has_vacancy_id
        ])
        self._budget_positions_for_vacancy_changes_cache = {
            vacancy_id: BudgetPositionMove(budget_position, budget_position)
            for vacancy_id, budget_position in vacancy_headcount_positions.items()
        }
        return self._budget_positions_for_vacancy_changes_cache

    def _budget_positions_for_free_headcount_changes(self) -> BudgetPositionCalculationResults:
        if self._budget_positions_for_free_headcount_changes_cache is not None:
            return self._budget_positions_for_free_headcount_changes_cache

        free_headcount_positions = self._budget_positions_repo.budget_positions_by_codes([
            proposal_data.headcount_position_code
            for proposal_data in self._proposal_data_list
            if proposal_data.headcount_position_code
        ])
        self._budget_positions_for_free_headcount_changes_cache = {
            bp.code: BudgetPositionMove(bp, bp)
            for bp in free_headcount_positions
        }
        return self._budget_positions_for_free_headcount_changes_cache

    def budget_position_move(self, proposal_data: ProposalData) -> BudgetPositionMove:
        return (
            self.budget_positions_for_person_changes().get(proposal_data.person_id) or
            self._budget_positions_for_vacancy_changes().get(proposal_data.vacancy_id) or
            self._budget_positions_for_free_headcount_changes().get(proposal_data.headcount_position_code) or
            BudgetPositionMove(None, None)
        )
