from typing import Union, Optional

from fastapi_utils.cbv import cbv
from fastapi_utils.inferring_router import InferringRouter
from fastapi import status

from watcher.logic.exceptions import (
    BadRequest,
    PermissionDenied,
    CompositionSlugMustBeUnique,
)
from watcher.api.schemas.composition import (
    CompositionCreateSchema,
    CompositionListSchema,
    CompositionPatchSchema,
    CompositionGetSchema,
)
from watcher.tasks.people_allocation import start_people_allocation
from watcher.tasks.composition import update_composition
from watcher.crud.composition import (
    patch_composition,
    create_composition,
)
from watcher.crud.schedule import query_schedules_by_composition
from watcher.crud.schedule_group import query_schedules_groups_by_composition
from watcher.logic.service import (
    check_roles_service,
    check_staff_service,
    is_user_in_service,
    is_responsible_in_service
)
from watcher.logic.permissions import (
    is_superuser,
    is_user_in_service_or_responsible,
)
from watcher.api.schemas.base import CursorPaginationResponse
from watcher.db import Composition

from .base import BaseRoute

router = InferringRouter()


@cbv(router)
class CompositionRoute(BaseRoute):
    model = Composition
    joined_load = (
        'roles', 'scopes', 'staff',
        'excluded_roles', 'excluded_scopes',
        'excluded_staff', 'participants',
    )

    joined_load_list = (
        'participants',
    )

    def _check_create_permissions(self, service_id: int):
        if not is_user_in_service_or_responsible(
            db=self.session,
            staff=self.current_user,
            service_id=service_id,
        ):
            raise PermissionDenied(message={
                'ru': 'Нет разрешения на создание состава',
                'en': 'No permission to create composition',
            })

    def _check_change_permissions(self):
        composition = self.get_object(self.request.path_params['composition_id'])
        service_id = composition.service_id
        if not is_superuser(staff=self.current_user):
            if not is_responsible_in_service(db=self.session, staff_id=self.current_user.id, service_id=service_id):
                if is_user_in_service(db=self.session, staff_id=self.current_user.id, service_id=service_id):
                    if self.request.method != 'DELETE' and self._is_composition_used(composition=composition):
                        raise PermissionDenied(message={
                            'ru': 'Нет разрешения на изменение используемого состава',
                            'en': 'No permission to change composition being used',
                        })
                else:
                    raise PermissionDenied(message={
                        'ru': 'Нет разрешения на изменение состава',
                        'en': 'No permission to change composition',
                    })

    @router.get('/{composition_id}')
    def retrieve(self, composition_id: int) -> CompositionGetSchema:
        obj = self.get_object(object_id=composition_id)
        if 'schedules' in self.fields_params:
            schedules = query_schedules_by_composition(
                db=self.session,
                composition_id=obj.id,
                service_id=obj.service_id,
            ).all()
            setattr(obj, 'schedules', schedules)
        return obj

    @router.delete('/{composition_id}', status_code=status.HTTP_204_NO_CONTENT)
    def destroy(self, composition_id: int):
        obj = self.get_object(object_id=composition_id)
        self._check_composition_unused(composition=obj)
        self.session.delete(obj)

    @router.get('/')
    def list(self) -> CursorPaginationResponse[CompositionListSchema]:
        return self.list_objects()

    @router.post('/{composition_id}/recalculate')
    def recalculate(self, composition_id: int) -> CompositionGetSchema:
        update_composition(
            composition_id=composition_id,
            force=True,
            _lock=False,
        )
        obj = self.get_object(object_id=composition_id)
        for schedule_group in query_schedules_groups_by_composition(db=self.session, composition_id=composition_id):
            start_people_allocation.delay(schedules_group_id=schedule_group.id)
        return obj

    @router.patch('/{composition_id}')
    def patch(self, composition_id: int, composition: CompositionPatchSchema) -> CompositionGetSchema:
        obj = self.get_object(object_id=composition_id)
        self._validate_schema(obj=obj, composition=composition)
        patched_obj, has_changes = patch_composition(
            db=self.session,
            obj=obj,
            composition=composition,
        )
        self.session.commit()
        if has_changes:
            update_composition(
                composition_id=patched_obj.id,
                force=True,
                _lock=False,
            )
            self.session.refresh(patched_obj)
        return patched_obj

    @router.post('/', status_code=status.HTTP_201_CREATED)
    def create(self, composition: CompositionCreateSchema) -> CompositionGetSchema:
        self._check_create_permissions(composition.service_id)

        self._validate_schema(composition=composition)
        self._validate_unique_slug(service_id=composition.service_id, slug=composition.slug)
        composition_obj = create_composition(
            db=self.session,
            composition=composition,
        )

        # подгрузим связанные модели
        composition_obj = self.get_object(object_id=composition_obj.id)
        update_composition(
            composition_id=composition_obj.id,
            force=True,
            _lock=False,
        )
        self.session.refresh(composition_obj)

        return composition_obj

    def _is_composition_used(self, composition: Composition) -> bool:
        return self.session.query(
            query_schedules_by_composition(
                db=self.session,
                composition_id=composition.id,
                service_id=composition.service_id
            ).exists()
        ).scalar()

    def _check_composition_unused(self, composition: Composition):
        if self._is_composition_used(composition=composition):
            raise BadRequest(
                message={
                    'ru': 'Состав используется в расписаниях',
                    'en': 'Composition is used in schedules',
                }
            )

    def _validate_schema(
        self,
        composition: Union[CompositionPatchSchema, CompositionCreateSchema],
        obj: Optional[Composition] = None
    ) -> None:

        # проверим что переданы роли/стафф из связанного сервиса
        service_id = obj.service_id if obj else composition.service_id
        for role_field in ['roles', 'excluded_roles']:
            role_field_value = getattr(composition, role_field)
            if role_field_value:
                check_roles_service(
                    db=self.session,
                    roles_ids=role_field_value,
                    expected=service_id,
                )
        if composition.staff:
            check_staff_service(
                db=self.session,
                staff_ids=composition.staff,
                expected=service_id,
            )

        # проверить что настройка full_service не противоречит roles/scopes/staff
        if composition.full_service is not False:
            full_service = composition.full_service is True or getattr(obj, 'full_service', False)
            if full_service:
                for attr in ['roles', 'scopes', 'staff']:
                    has_attr = getattr(composition, attr)
                    if has_attr:
                        raise BadRequest(
                            message={
                                'ru': 'Настройка full_service конфликтует с остальными настройками',
                                'en': 'The full_service setting conflicts with the rest of the settings',
                            }
                        )

    def _validate_unique_slug(self, service_id: int, slug: str) -> None:
        compositions = self.session.query(Composition).filter(
            Composition.slug == slug,
            Composition.service_id == service_id,
        ).count()
        if compositions:
            raise CompositionSlugMustBeUnique
