import logging

from typing import Iterable, Tuple
from sqlalchemy.orm import Session

from watcher.enums import MemberState
from watcher.logic.timezone import now
from watcher.db.base import dbconnect
from watcher.crud.rating import (
    get_or_create_ratings_by_schedule_staff_ids,
)
from watcher.crud.schedule import query_schedules_by_composition
from watcher.crud.composition import (
    get_composition,
    update_many_to_many_for_field,
    query_need_recalculation_compositions,
    query_check_staff_compositions,
)
from watcher.crud.member import (
    query_target_composition_staff,
    query_target_composition_staff_excluded,
)
from watcher.crud.schedule_group import query_schedules_groups_by_composition
from watcher.db import CompositionParticipants, Composition, Member, CompositionToStaff, CompositionToStaffExcluded
from watcher.config import settings
from watcher.crud.role import get_role_by_code
from .base import lock_task
from .process_delete_members import (
    _find_affected_shifts,
    _start_people_reallocation,
)
logger = logging.getLogger(__name__)


def _get_target_refs(
    composition: Composition, excluded_scopes: Iterable,
    excluded_roles: Iterable, excluded_staff: Iterable
) -> Tuple[set, set, set]:
    """
    Получаем целевые роли/скоупы/людей
    которые нужно сохранить в participants
    """

    scopes = {
        obj.id for obj in composition.scopes
        if obj.id not in excluded_scopes
    }

    roles = {
        obj.id for obj in composition.roles
        if obj.scope_id not in excluded_scopes
           and obj.scope_id not in scopes
           and obj.id not in excluded_roles
    }

    staff = {
        obj.id for obj in composition.staff
        if obj.id not in excluded_staff
    }

    return scopes, roles, staff


@lock_task(lock_key=lambda composition_id, *args, **kwargs: 'update_composition_{}'.format(composition_id))
@dbconnect
def update_composition(session: Session, composition_id: int, force: bool = False):
    """
    Разворачиваем состав композиции

    :param force - игнорируем настройку autoupdate
    """
    from watcher.tasks.people_allocation import start_people_allocation
    logger.info(f'Updating composition {composition_id}, force: {force}')

    composition = get_composition(db=session, composition_id=composition_id)
    if not (force or composition.autoupdate):
        composition.updated_at = now()
        logger.info(f'Skipping update composition for {composition.id}')
        return

    excluded_staff = [obj.id for obj in composition.excluded_staff]
    excluded_scopes = [obj.id for obj in composition.excluded_scopes]
    excluded_roles = [
        obj.id for obj in composition.excluded_roles
        if obj.scope_id not in excluded_scopes
    ]

    if not composition.full_service:
        scopes, roles, staff = _get_target_refs(
            composition=composition, excluded_scopes=excluded_scopes,
            excluded_roles=excluded_roles, excluded_staff=excluded_staff
        )
        target_participants = query_target_composition_staff(
            db=session, composition=composition, staff=staff,
            roles=roles, scopes=scopes, excluded_staff=excluded_staff,
            excluded_roles=excluded_roles, excluded_scopes=excluded_scopes,
        )
    else:
        responsible_role = get_role_by_code(db=session, code=settings.RESPONSIBLE_ROLE_CODE)
        excluded_roles.append(responsible_role.id)
        target_participants = query_target_composition_staff_excluded(
            db=session, composition=composition, excluded_staff=excluded_staff,
            excluded_roles=excluded_roles, excluded_scopes=excluded_scopes,
        )

    target_participants = {r for (r, ) in target_participants.all()}
    current_refs = (
        session.query(CompositionParticipants)
        .filter(CompositionParticipants.composition_id == composition.id)
        .all()
    )
    current_participants = {obj.staff_id for obj in current_refs}

    schedule_ids = [
        schedule.id for schedule in query_schedules_by_composition(
            db=session, service_id=composition.service_id,
            composition_id=composition.id,
        ).all()
    ]
    update_many_to_many_for_field(
        db=session, current=current_participants, target=target_participants,
        table=CompositionParticipants, current_refs=current_refs,
        field_key='staff_id', obj=composition, related_field='composition_id',
    )
    session.refresh(composition)  # иначе не сможем рейтинги получить корректно ниже

    get_or_create_ratings_by_schedule_staff_ids(
        db=session, schedule_ids=schedule_ids,
        staff_ids=target_participants,
    )
    composition.updated_at = now()

    if current_participants != target_participants:
        for schedule_group in query_schedules_groups_by_composition(db=session, composition_id=composition.id):
            start_people_allocation.delay(schedules_group_id=schedule_group.id)

    logger.info(f'Finish updating composition {composition_id}')


@lock_task(save_metrics=True, send_to_unistat=True)
@dbconnect
def update_compositions(session: Session):
    logger.info('Starting updating compositions')
    compositions = query_check_staff_compositions(db=session)
    for composition in compositions:
        check_composition_staff(composition_id=composition.id, _lock=False)

    compositions = query_need_recalculation_compositions(db=session)
    for composition in compositions:
        update_composition(composition_id=composition.id, _lock=False)
    logger.info('Finish updating compositions')


@lock_task(lock_key=lambda composition_id, *args, **kwargs: 'check_composition_staff_{}'.format(composition_id))
@dbconnect
def check_composition_staff(session: Session, composition_id: int):
    """
    Проверяем что в настройках композиций указаны люди, которые все еще относятся
    к этому сервису - если нет - удаляем их из настроек и запускаем пересчет
    """

    logger.info(f'Checking composition {composition_id}')

    composition = get_composition(db=session, composition_id=composition_id)
    if not composition.staff and not composition.excluded_staff:
        return

    query_member_ids = session.query(Member.staff_id).filter(
        Member.service_id == composition.service_id,
        Member.state.in_((MemberState.active, MemberState.depriving)),
    ).all()  # результат query будет вида [(2,), (3,)]

    service_staff_ids = set(staff_id for staff_id, in query_member_ids)

    need_update = False
    old_staff = {obj.id for obj in composition.staff}
    permitted_staff = old_staff.intersection(service_staff_ids)
    if old_staff != permitted_staff:
        need_update |= True

    old_excluded_staff = {obj.id for obj in composition.excluded_staff}
    permitted_excluded_staff = old_excluded_staff.intersection(service_staff_ids)
    if old_excluded_staff != permitted_excluded_staff:
        need_update |= True

    if not need_update:
        return

    participants_to_process = old_staff.difference(service_staff_ids)

    update_many_to_many_for_field(
        db=session, current=old_staff, target=permitted_staff,
        table=CompositionToStaff,
        current_refs=session.query(CompositionToStaff)
            .filter(CompositionToStaff.composition_id == composition.id).all(),
        field_key='staff_id', obj=composition, related_field='composition_id',
    )
    update_many_to_many_for_field(
        db=session, current=old_excluded_staff, target=permitted_excluded_staff,
        table=CompositionToStaffExcluded,
        current_refs=session.query(CompositionToStaffExcluded)
            .filter(CompositionToStaffExcluded.composition_id == composition.id).all(),
        field_key='staff_id', obj=composition, related_field='composition_id',
    )

    session.query(CompositionParticipants).filter(
        CompositionParticipants.composition_id==composition_id,
        CompositionParticipants.staff_id.in_(participants_to_process)
    ).delete(synchronize_session=False)

    shifts = _find_affected_shifts(session=session, participants={composition_id: participants_to_process})
    if shifts:
        _start_people_reallocation(
            session=session,
            affected_shifts=shifts,
        )
