from datetime import datetime
from typing import Any, Dict, Iterable, List, Optional

import attr

from django.db.models import QuerySet

from staff.departments.models import DepartmentRoles, HeadcountPosition, Vacancy, Geography
from staff.budget_position.models import BudgetPosition
from staff.lib.models.roles_chain import get_roles_by_departments_qs
from staff.lib.utils.qs_values import extract_related
from staff.person.models import Staff

from staff.headcounts.headcounts_credit_management import use_cases
from staff.headcounts.headcounts_credit_management.use_cases import dto
from staff.headcounts.models import CreditManagementApplication, CreditManagementApplicationRow


@attr.s(cmp=False, auto_attribs=True)
class BudgetPositionCreationData:
    codes: Dict[int, int]
    departments: Dict[int, Any]
    vacancies: Dict[int, Any]
    valuestreams: Dict[int, Any]
    geographies: Dict[int, dto.GeographyDescription]


class Repository(use_cases.RepositoryInterface):
    def __init__(self, author: Optional[Staff]):
        self._author = author

    def save(self, create_repayment_request: use_cases.CreateCreditRepaymentRequest) -> int:
        assert self._author is not None

        application_model = CreditManagementApplication.objects.create(
            author=self._author,
            comment=create_repayment_request.comment,
        )

        objects_to_create = [
            CreditManagementApplicationRow(
                application=application_model,
                credit_budget_position_id=row.credit_budget_position_id,
                repayment_budget_position_id=row.repayment_budget_position_id,
            )
            for row in create_repayment_request.rows
        ]
        CreditManagementApplicationRow.objects.bulk_create(objects_to_create)
        return application_model.id

    def save_credit_repayment(self, credit_repayment: dto.CreditRepayment) -> None:
        # пока мы здесь не обновляем строки заявки, допишем потом когда надо будет
        assert credit_repayment.id is not None
        author = Staff.objects.get(login=credit_repayment.author_login)
        CreditManagementApplication.objects.filter(id=credit_repayment.id).update(
            author=author,
            closed_at=credit_repayment.closed_at,
            comment=credit_repayment.comment,
            startrek_headcount_key=credit_repayment.ticket,
            is_active=credit_repayment.is_active,
        )

    def _involved_budget_positions(self, application: CreditManagementApplication) -> Dict[int, int]:
        ids = set()

        for row in application.rows.all():
            ids.add(row.credit_budget_position_id)
            ids.add(row.repayment_budget_position_id)

        return {
            bp_id: bp_code
            for bp_id, bp_code in BudgetPosition.objects.filter(id__in=ids).values_list('id', 'code')
        }

    def _responsible_for_department(self, department: Dict[str, int], role: str) -> str:
        department_role = get_roles_by_departments_qs([department], [role], ('login',)).first()
        return department_role and department_role['staff__login']

    def _budget_positions_departments(self, codes: Iterable[int]) -> Dict[int, Any]:
        positions = (
            HeadcountPosition.objects
            .filter(code__in=codes)
            .values('code', 'department__lft', 'department__rght', 'department__tree_id', 'department__url')
        )
        return {
            position['code']: extract_related(position, 'department')
            for position in positions
        }

    def _budget_positions_valuestreams(self, codes: Iterable[int]) -> Dict[int, str]:
        result = dict(HeadcountPosition.objects.filter(code__in=codes).values_list('code', 'valuestream__name'))
        return result

    def _budget_positions_geography(self, codes: Iterable[int]) -> Dict[int, dto.GeographyDescription]:
        positions = dict(HeadcountPosition.objects.filter(code__in=codes).values_list('code', 'geo'))
        geography_names = {
            g.oebs_code: g.name
            for g in Geography.objects.filter(oebs_code__in=positions.values())
        }
        return {
            code: dto.GeographyDescription(name=geography_names.get(geo_code, ''), oebs_code=geo_code)
            for code, geo_code in positions.items()
        }

    def _to_budget_position(
        self,
        budget_position_id: int,
        creation_data: BudgetPositionCreationData,
    ) -> dto.BudgetPosition:
        code = creation_data.codes[budget_position_id]
        department = creation_data.departments.get(code)
        vacancy_id = creation_data.vacancies.get(code)
        valuestream = creation_data.valuestreams.get(code)
        geography = creation_data.geographies.get(code)

        if not department:
            return dto.BudgetPosition(
                code=code,
                department_url=None,
                hr_analyst_login=None,
                hr_partner_login=None,
                vacancy_id=vacancy_id,
                valuestream=valuestream,
                geography=geography,
            )

        hr_partner = self._responsible_for_department(department, DepartmentRoles.HR_PARTNER.value)
        hr_analyst = self._responsible_for_department(department, DepartmentRoles.HR_ANALYST.value)
        return dto.BudgetPosition(
            code=code,
            department_url=department['url'],
            hr_partner_login=hr_partner,
            hr_analyst_login=hr_analyst,
            vacancy_id=vacancy_id,
            valuestream=valuestream,
            geography=geography,
        )

    def _vacancies(self, codes: Iterable[int]) -> Dict[int, str]:
        vacancies_qs = (
            Vacancy.objects
            .filter(headcount_position_code__in=codes, is_active=True)
            .values_list('id', 'headcount_position_code')
        )
        return {code: vacancy_id for vacancy_id, code in vacancies_qs}

    def _to_dto(self, application: CreditManagementApplication) -> dto.CreditRepayment:
        rows = []

        budget_positions_codes = self._involved_budget_positions(application)
        creation_data = BudgetPositionCreationData(
            codes=budget_positions_codes,
            departments=self._budget_positions_departments(budget_positions_codes.values()),
            vacancies=self._vacancies(budget_positions_codes.values()),
            valuestreams=self._budget_positions_valuestreams(budget_positions_codes.values()),
            geographies=self._budget_positions_geography(budget_positions_codes.values()),
        )

        for row in application.rows.all():
            rows.append(dto.CreditRepaymentRow(
                credit_budget_position=self._to_budget_position(row.credit_budget_position_id, creation_data),
                repayment_budget_position=self._to_budget_position(row.repayment_budget_position_id, creation_data),
            ))

        return dto.CreditRepayment(
            id=application.id,
            author_login=application.author.login,
            comment=application.comment,
            rows=rows,
            ticket=application.startrek_headcount_key,
            is_active=application.is_active,
            closed_at=application.closed_at,
        )

    def get_by_id(self, id: int) -> dto.CreditRepayment:
        applications_qs = (
            CreditManagementApplication.objects
            .filter(id=id)
            .prefetch_related('rows')
            .select_related('author')
        )
        application = applications_qs.first()
        return application and self._to_dto(application)

    def list(self) -> QuerySet:
        fields = (
            'id',
            'created_at',
            'comment',
            'startrek_headcount_key',
            'is_active',
            'author__login',
            'author__first_name',
            'author__last_name',
            'author__first_name_en',
            'author__last_name_en',
        )
        applications_qs = (
            CreditManagementApplication.objects
            .filter(author=self._author)
            .values(*fields)
            .order_by('-created_at')
        )

        return applications_qs

    def close_application(self, credit_repayment_application_id: int) -> None:
        CreditManagementApplication.objects.filter(id=credit_repayment_application_id).update(closed_at=datetime.now())

    def deactivate_application(self, credit_repayment_application_id: int) -> None:
        CreditManagementApplication.objects.filter(id=credit_repayment_application_id).update(is_active=False)

    def closed_but_active_applications(self) -> List[dto.CreditRepayment]:
        qs = CreditManagementApplication.objects.exclude(closed_at=None).filter(is_active=True)
        return [self._to_dto(cma) for cma in qs]

    def applications_without_ticket(self) -> List[dto.CreditRepayment]:
        qs = CreditManagementApplication.objects.filter(startrek_headcount_key=None, is_active=True)
        return [self._to_dto(cma) for cma in qs]
