from typing import List, Any

import sform

from django.core.exceptions import ValidationError
from django.db.models import Q
from django.http import QueryDict

from staff.budget_position.models import (
    BudgetPosition,
    BudgetPositionAssignment,
    BudgetPositionAssignmentStatus,
    ReplacementType,
    RewardCategory,
)
from staff.departments.models import Department
from staff.lib.utils.ordered_choices import OrderedChoices
from staff.oebs.constants import PERSON_POSITION_STATUS, REPLACEMENT_TYPE
from staff.person.models import Staff

from staff.headcounts.headcounts_summary.query_builder.query_params import RelatedEntity, HierarchyValues
from staff.headcounts.models import CreditManagementApplicationRow


QUANTITY_FILTER = OrderedChoices(
    ('ZERO', 'zero'),
    ('GREATER_THAN_ZERO', 'gt_zero'),
    ('LESS_THAN_ZERO', 'lt_zero'),
)


class OldHeadcountsFilterForm(sform.SForm):
    position_statuses = sform.MultipleChoiceField(choices=PERSON_POSITION_STATUS, state=sform.NORMAL)
    category_is_new = sform.NullBooleanField(state=sform.NORMAL)
    is_crossing = sform.NullBooleanField(state=sform.NORMAL)
    main_assignment = sform.NullBooleanField(state=sform.NORMAL)
    replacement_type = sform.MultipleChoiceField(choices=REPLACEMENT_TYPE, state=sform.NORMAL)
    quantity = sform.ChoiceField(choices=QUANTITY_FILTER, state=sform.NORMAL)
    department = sform.SuggestField(
        queryset=Department.objects.all(),
        to_field_name='url',
        label_fields=('name',),
    )
    value_stream = sform.SuggestField(
        queryset=Department.valuestreams.all(),
        to_field_name='url',
        label_fields=('name',),
    )
    geography = sform.SuggestField(
        queryset=Department.geography.all(),
        to_field_name='url',
        label_fields=('name',),
    )
    current_person = sform.SuggestField(
        queryset=Staff.objects.filter(is_robot=False),
        to_field_name='login',
        label_fields=('first_name', 'last_name',),
    )
    replaced_person = sform.SuggestField(
        queryset=Staff.objects.filter(is_robot=False),
        to_field_name='login',
        label_fields=('first_name', 'last_name',),
    )
    code = sform.IntegerField(state=sform.NORMAL)
    search_text = sform.CharField()
    category = sform.GridField(sform.ChoiceField(choices=RewardCategory.choices()))

    @classmethod
    def from_query_dict(cls, query_dict):
        result = {}

        for field_name, field in cls.base_fields.items():
            if isinstance(field, sform.MultipleChoiceField) or isinstance(field, sform.GridField):
                result[field_name] = query_dict.getlist(field_name, None)
            else:
                result[field_name] = query_dict.get(field_name, None)

        return cls(data=result)

    def clean_search_text(self, value):
        return value.strip()


class HeadcountsFilterForm(sform.SForm):
    position_statuses = sform.MultipleChoiceField(choices=BudgetPositionAssignmentStatus.choices(), state=sform.NORMAL)
    category_is_new = sform.NullBooleanField(state=sform.NORMAL)
    is_crossing = sform.NullBooleanField(state=sform.NORMAL)
    main_assignment = sform.NullBooleanField(state=sform.NORMAL)
    replacement_type = sform.MultipleChoiceField(choices=ReplacementType.choices(), state=sform.NORMAL)
    quantity = sform.ChoiceField(choices=QUANTITY_FILTER, state=sform.NORMAL)
    department = sform.SuggestField(
        queryset=Department.objects.all(),
        to_field_name='url',
        label_fields=('name',),
    )
    value_stream = sform.SuggestField(
        queryset=Department.valuestreams.all(),
        to_field_name='url',
        label_fields=('name',),
    )
    geography = sform.SuggestField(
        queryset=Department.geography.all(),
        to_field_name='url',
        label_fields=('name',),
    )
    current_person = sform.SuggestField(
        queryset=Staff.objects.filter(is_robot=False),
        to_field_name='login',
        label_fields=('first_name', 'last_name',),
    )
    replaced_person = sform.SuggestField(
        queryset=Staff.objects.filter(is_robot=False),
        to_field_name='login',
        label_fields=('first_name', 'last_name',),
    )
    code = sform.IntegerField(state=sform.NORMAL)
    search_text = sform.CharField()
    category = sform.GridField(sform.ChoiceField(choices=RewardCategory.choices()))

    @classmethod
    def from_query_dict(cls, query_dict):
        result = {}

        for field_name, field in cls.base_fields.items():
            if isinstance(field, sform.MultipleChoiceField) or isinstance(field, sform.GridField):
                result[field_name] = query_dict.getlist(field_name, None)
            else:
                result[field_name] = query_dict.get(field_name, None)

            # TODO: STAFF-17443: Remove
            if field_name == 'position_statuses':
                result[field_name] = [x.lower() for x in result[field_name]]
            elif field_name == 'replacement_type':
                def _align_replacement_type(v):
                    return {
                        'HC_IS_BUSY': 'busy',
                        'HAS_REPLACEMENT_AND_HC_IS_BUSY': 'has_replacement_and_busy',
                        'WO_REPLACEMENT': 'without_replacement',
                    }.get(v, v.lower())
                result[field_name] = [_align_replacement_type(x) for x in result[field_name]]

        return cls(data=result)

    def clean_search_text(self, value):
        return value.strip()


class CreditManagementApplicationRowForm(sform.SForm):
    credit = sform.SuggestField(
        queryset=BudgetPosition.objects.filter(headcount__lt=0),
        state=sform.REQUIRED,
        label_fields={'caption': ('id',)},
        to_field_name='code',
    )
    repayment = sform.SuggestField(
        queryset=BudgetPosition.objects.filter(headcount__gt=0),
        state=sform.REQUIRED,
        label_fields={'caption': ('id',)},
        to_field_name='code',
    )

    def _check_budget_position_alredy_used(self, value: BudgetPosition) -> None:
        filter_q = Q(credit_budget_position=value) | Q(repayment_budget_position=value)
        active_application_with_same_position = (
            CreditManagementApplicationRow.objects
            .filter(filter_q, application__is_active=True)
        )

        if not active_application_with_same_position.exists():
            return

        raise sform.ValidationError(
            'Credit BP already used in another application',
            code='invalid_choice',
            params={
                'code': value.code,
                'other_application': active_application_with_same_position.first().application.id,
            },
        )

    def clean_credit(self, value: BudgetPosition) -> BudgetPosition:
        qs = BudgetPositionAssignment.objects.active().filter(budget_position=value)

        if len(qs) != 1:
            raise sform.ValidationError(
                'Should be exactly one active assignment for credit bp',
                code='invalid_choice',
                params={'code': value.code},
            )

        assignment_status_value = qs[0].status
        if assignment_status_value != BudgetPositionAssignmentStatus.RESERVE.value:
            raise sform.ValidationError(
                'Credit BP assignment should be in reserve status',
                code='invalid_choice',
                params={'code': value.code, 'status': assignment_status_value},
            )

        self._check_budget_position_alredy_used(value)
        return value

    def clean_repayment(self, value: BudgetPosition) -> BudgetPosition:
        qs = BudgetPositionAssignment.objects.active().filter(budget_position=value)

        if len(qs) != 1:
            raise sform.ValidationError(
                'Repayment BP should be without crossing',
                code='invalid_choice',
                params={'code': value.code},
            )

        assignment_status_value = qs[0].status
        allowed_statuses_for_repayment = (
            BudgetPositionAssignmentStatus.VACANCY_PLAN.value,
            BudgetPositionAssignmentStatus.VACANCY_OPEN.value,
        )
        if assignment_status_value not in allowed_statuses_for_repayment:
            raise sform.ValidationError(
                'Credit BP assignment should be in reserve status',
                code='invalid_choice',
                params={'code': value.code, 'status': assignment_status_value},
            )

        self._check_budget_position_alredy_used(value)
        return value


class CreditManagementApplicationForm(sform.SForm):
    comment = sform.CharField(state=sform.REQUIRED)
    budget_positions = sform.GridField(
        field_instance=sform.FieldsetField(CreditManagementApplicationRowForm),
        state=sform.REQUIRED,
    )


class MoveToMaternityForm(sform.SForm):
    budget_position = sform.SuggestField(
        queryset=BudgetPosition.objects.all(),
        state=sform.REQUIRED,
        label_fields={'caption': ('id',)},
        to_field_name='code',
    )

    person = sform.SuggestField(
        queryset=Staff.objects.filter(is_robot=False),
        state=sform.REQUIRED,
        to_field_name='login',
        label_fields=('first_name', 'last_name',),
    )

    def clean(self):
        cleaned_data = self.cleaned_data

        bp = cleaned_data.get('budget_position')
        person: Staff = cleaned_data.get('person')

        if bp and person:
            if person.budget_position != bp:
                raise ValidationError('No such person on bp', code='no_person_on_bp')

        return super().clean()


class MultiplePersonsForm(sform.SForm):
    persons = sform.MultipleSuggestField(
        queryset=Staff.objects.filter(is_dismissed=False),
        to_field_name='login',
        label_fields={
            'caption': ('first_name', 'last_name'),
            'extra_fields': ['login'],
        },
        state=sform.REQUIRED,
    )


class BudgetPositionAssignmentGroupingAndFiltersForm(sform.SForm):
    groupings = sform.GridField(sform.ChoiceField(RelatedEntity.choices()), state=sform.NORMAL)
    department_filters = sform.GridField(
        sform.ModelChoiceField(
            queryset=Department.objects.all(),
            state=sform.REQUIRED,
            to_field_name='url',
            label_extractor='name',
        )
    )

    value_stream_filters = sform.GridField(
        sform.ModelChoiceField(
            queryset=Department.valuestreams.all(),
            state=sform.REQUIRED,
            to_field_name='url',
            label_extractor='name',
        )
    )

    geography_filters = sform.GridField(
        sform.ModelChoiceField(
            queryset=Department.geography.all(),
            state=sform.REQUIRED,
            to_field_name='url',
            label_extractor='name',
        )
    )

    debug = sform.IntegerField(state=sform.NORMAL, default=0)

    @classmethod
    def from_query_dict(cls, query_dict: QueryDict):
        result = {}

        for field_name, field in cls.base_fields.items():
            if isinstance(field, sform.MultipleChoiceField) or isinstance(field, sform.GridField):
                result[field_name] = query_dict.getlist(field_name, None)
            else:
                result[field_name] = query_dict.get(field_name, None)

        return cls(data=result)

    def clean_debug(self, value: Any) -> bool:
        return bool(value)

    def clean_groupings(self, values: List[str]) -> List[RelatedEntity]:
        return [RelatedEntity(value) for value in values]

    def _clean_departments_models(self, values: List[Department]):
        return [
            HierarchyValues(lft=dep.lft, rght=dep.rght, tree_id=dep.tree_id, id=dep.id)
            for dep in values
        ]

    def clean_department_filters(self, values: List[Department]) -> List[HierarchyValues]:
        return self._clean_departments_models(values)

    def clean_value_stream_filters(self, values: List[Department]) -> List[HierarchyValues]:
        return self._clean_departments_models(values)

    def clean_geography_filters(self, values: List[Department]) -> List[HierarchyValues]:
        return self._clean_departments_models(values)

    def is_debug(self) -> bool:
        assert self.is_valid()
        return self.cleaned_data['debug']
