import datetime

from typing import Optional, Tuple, List, Iterable
from sqlalchemy.orm import Session, Query, joinedload
from sqlalchemy import or_

from watcher.api.schemas.composition import (
    CompositionPatchSchema,
    CompositionCreateSchema,
)
from watcher.config import settings
from watcher.crud.base import (
    commit_object,
    _set_fields,
    _bulk_insert_refs,
    _remove_relationships,
    get_object_by_model_or_404,
    update_many_to_many_for_field,
)
from watcher.db import (
    Composition,
    CompositionParticipants,
    CompositionToRole,
    CompositionToRoleExcluded,
    CompositionToScope,
    CompositionToScopeExcluded,
    CompositionToStaff,
    CompositionToStaffExcluded,
    Staff, Slot,
    Interval, Schedule,
)
from watcher.logic.timezone import now

FULL_SERVICE_REMOVE = ['roles', 'scopes', 'staff']

MANY_TO_MANY_FIELDS = {
    'roles': (CompositionToRole, 'role_id'),
    'scopes': (CompositionToScope, 'scope_id'),
    'staff': (CompositionToStaff, 'staff_id'),
    'excluded_roles': (CompositionToRoleExcluded, 'role_id'),
    'excluded_scopes': (CompositionToScopeExcluded, 'scope_id'),
    'excluded_staff': (CompositionToStaffExcluded, 'staff_id'),
}


def _update_many_to_many_for_composition(db: Session, obj: Composition, many_to_many: dict) -> None:
    for field, values in many_to_many.items():
        table, field_key = MANY_TO_MANY_FIELDS[field]
        values = set(values)

        current_refs = db.query(table).filter(table.composition_id==obj.id).all()
        current_values = {getattr(ref, field_key) for ref in current_refs}

        update_many_to_many_for_field(
            db=db, current=current_values, target=values,
            table=table, current_refs=current_refs,
            field_key=field_key, obj=obj, related_field='composition_id',
        )


def _set_null_on_refs(dict_obj: dict) -> None:
    """
    Если full_service выставлен в True - нужно
    занулить другие настройки ролей/скоупов/людей,
    кроме исключений
    """
    for field in FULL_SERVICE_REMOVE:
        dict_obj[field] = []


def patch_composition(db: Session, obj: Composition, composition: CompositionPatchSchema) -> Tuple[Composition, bool]:
    dict_obj = composition.dict(exclude_unset=True)
    if dict_obj.get('full_service'):
        _set_null_on_refs(dict_obj)
    many_to_many = _remove_relationships(dict_obj=dict_obj, relationship_fields=MANY_TO_MANY_FIELDS)
    has_changes = _set_fields(obj=obj, update_data=dict_obj)
    _update_many_to_many_for_composition(db=db, obj=obj, many_to_many=many_to_many)

    return obj, bool(has_changes or many_to_many)


def create_composition(db: Session, composition: CompositionCreateSchema) -> Composition:
    dict_obj = composition.dict()
    many_to_many = _remove_relationships(dict_obj=dict_obj, relationship_fields=MANY_TO_MANY_FIELDS)

    obj = Composition(**dict_obj)
    db_obj = commit_object(db=db, obj=obj)

    for field, values in many_to_many.items():
        if values:
            table, field_key = MANY_TO_MANY_FIELDS[field]
            _bulk_insert_refs(
                db=db, obj=db_obj,
                field_key=field_key,
                to_add=values, table=table,
                related_field='composition_id',
            )

    db.commit()
    return db_obj


def query_participants_by_schedules_group(db: Session, schedules_group_id: int) -> Query:
    return (
        db.query(Staff)
        .join(CompositionParticipants, CompositionParticipants.staff_id == Staff.id)
        .join(Composition, Composition.id == CompositionParticipants.composition_id)
        .join(Slot, Slot.composition_id == Composition.id)
        .join(Interval, Interval.id == Slot.interval_id)
        .join(Schedule, Schedule.id == Interval.schedule_id)
        .filter(Schedule.schedules_group_id == schedules_group_id)
    )


def query_need_recalculation_compositions(db: Session, query: Optional[Query] = None) -> Query:
    if not query:
        query = db.query(Composition)

    threshold = now() - datetime.timedelta(seconds=settings.TOTAL_SECONDS_FOR_UPDATING_COMPOSITION)

    return query.filter(Composition.updated_at <= threshold, Composition.autoupdate.is_(True))


def query_check_staff_compositions(db: Session):
    return (
        db.query(Composition)
        .filter(
            or_(
                Composition.staff.any(),
                Composition.excluded_staff.any()
            )
        )
    )


def query_compositions_by_schedules_group(db: Session, schedules_group_id: int) -> Query:
    query = (
        db.query(Composition)
        .join(Slot, Slot.composition_id == Composition.id)
        .join(Interval, Interval.id == Slot.interval_id)
        .join(Schedule, Schedule.id == Interval.schedule_id)
        .filter(Schedule.schedules_group_id == schedules_group_id)
    )

    return query_need_recalculation_compositions(db, query)


def query_participants_by_composition(db: Session, composition_id: int) -> Query:
    return db.query(CompositionParticipants).filter(CompositionParticipants.composition_id == composition_id)


def query_participants_by_composition_and_staff_ids(db: Session, composition_id: int, staff_ids: Iterable[int]) -> Query:
    return (
        query_participants_by_composition(db=db, composition_id=composition_id)
        .filter(CompositionParticipants.staff_id.in_(staff_ids))
    )


def get_composition(db: Session, composition_id: int) -> Composition:
    return get_object_by_model_or_404(
        db=db, model=Composition,
        object_id=composition_id,
        joined_load=(
            'roles', 'scopes', 'staff',
            'excluded_roles', 'excluded_scopes',
            'excluded_staff', 'participants',
        )
    )


def get_compositions_by_ids(db: Session, composition_ids: List[int]) -> List[Composition]:

    return db.query(Composition).filter(Composition.id.in_(composition_ids)).options(
        joinedload(Composition.participants)
    ).all()


def delete_members_from_composition(db: Session, composition_id: int, staff_ids: Iterable[int]) -> None:
    (
        db.query(CompositionParticipants)
        .filter(CompositionParticipants.staff_id.in_(staff_ids))
        .filter(Composition.id == CompositionParticipants.composition_id)
        .filter(Composition.id == composition_id)
        .delete(synchronize_session=False)
    )
    (
        db.query(CompositionToStaff)
        .filter(Composition.id == CompositionToStaff.composition_id)
        .filter(Composition.id == composition_id)
        .filter(CompositionToStaff.staff_id.in_(staff_ids))
        .delete(synchronize_session=False)
    )
    (
        db.query(CompositionToStaffExcluded)
        .filter(Composition.id == CompositionToStaffExcluded.composition_id)
        .filter(Composition.id == composition_id)
        .filter(CompositionToStaffExcluded.staff_id.in_(staff_ids))
        .delete(synchronize_session=False)
    )


def query_compositions_by_services(db: Session, service_ids: Iterable[int], query: Optional[Query] = None) -> Query:
    if not query:
        query = db.query(Composition)

    return query.filter(Composition.service_id.in_(service_ids)).options(joinedload(Composition.participants))
