from sqlalchemy import or_
from sqlalchemy.orm import Session, Query
from typing import Optional, Callable, Iterable

from watcher import enums
from watcher.api.schemas.schedulegroup import ScheduleGroupCreateSchema
from watcher.crud.base import (
    get_object_by_model,
    get_object_by_model_or_404,
    commit_object,
)
from watcher.crud.schedule import query_schedules_by_group
from watcher.db import (
    Interval,
    Schedule,
    SchedulesGroup,
    Shift,
    Slot,
    Staff,
    Composition,
    CompositionParticipants,
)
from watcher.logic.timezone import now


def query_groups_without_schedule(db: Session) -> Query:
    return (
        db.query(SchedulesGroup)
        .outerjoin(Schedule, Schedule.schedules_group_id == SchedulesGroup.id)
        .filter(Schedule.id.is_(None))
    )


def query_autogenerated_groups_without_schedule(db: Session) -> Query:
    return query_groups_without_schedule(db).filter(SchedulesGroup.autogenerated)


def query_groups_with_allocation_error(db: Session) -> Query:
    return (
        db.query(SchedulesGroup, Shift)
        .join(Schedule, Schedule.schedules_group_id == SchedulesGroup.id)
        .join(Shift, Shift.schedule_id == Schedule.id)
        .filter(
            or_(
                SchedulesGroup.people_allocation_error.isnot(None),
                SchedulesGroup.last_people_allocation_at.is_(None),
            )
        )
    )


def create_schedules_group(db: Session, schedulegroup: ScheduleGroupCreateSchema, author: Staff) -> SchedulesGroup:
    obj = SchedulesGroup(responsibles=[author], **schedulegroup.dict())
    db_obj = commit_object(db=db, obj=obj)
    return db_obj


def get_schedules_group_by_id(db: Session, schedules_group_id: int) -> Schedule:
    return get_object_by_model(db=db, model=SchedulesGroup, object_id=schedules_group_id)


def get_schedules_group_or_404(db: Session, schedules_group_id: int) -> Schedule:
    return get_object_by_model_or_404(db=db, model=SchedulesGroup, object_id=schedules_group_id)


def save_schedules_group_allocation_error(
    db: Session,
    schedules_group_id: int,
    message: str,
    raise_exception: Optional[Callable] = None
) -> None:
    error = {
        'message': message,
        'datetime': now().strftime('%Y-%m-%d %H:%M:%S %Z'),
    }
    schedules_group = get_schedules_group_or_404(db, schedules_group_id)
    schedules_group.people_allocation_error = error
    db.commit()

    if raise_exception:
        raise raise_exception(message=message)


def schedules_group_validate_destroy(db: Session, schedules_group_id: int) -> bool:
    return not db.query(query_schedules_by_group(db, schedules_group_id).exists()).scalar()


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


def query_schedule_groups_for_staff_ids(db: Session, staff_ids: Iterable[int]) -> Query:
    return (
        db.query(SchedulesGroup)
        .join(Schedule, Schedule.schedules_group_id == SchedulesGroup.id)
        .join(Interval, Interval.schedule_id == Schedule.id)
        .join(Slot, Slot.interval_id == Interval.id)
        .join(Composition, Composition.id == Slot.composition_id)
        .join(CompositionParticipants, CompositionParticipants.composition_id == Composition.id)
        .join(Staff, Staff.id == CompositionParticipants.staff_id)
        .filter(Staff.id.in_(staff_ids))
    )


def schedule_group_has_employed_intervals(db: Session, schedules_group_id: int) -> bool:
    return db.query(
        query_schedules_by_group(db, schedules_group_id=schedules_group_id)
        .join(Interval, Interval.schedule_id == Schedule.id)
        .filter(Interval.type_employment == enums.IntervalTypeEmployment.full)
        .exists()
    ).scalar()
