from typing import Optional, Callable, Iterable

from sqlalchemy import func
from sqlalchemy.orm import Session, Query

from watcher import enums

from watcher.api.schemas.schedule import (
    ScheduleCreateSchema,
    ScheduleWithValidatorSchema,
)
from watcher.db import ScheduleShowInServices
from watcher.logic.schedule_group import create_autogenerated_group_object
from watcher.db import (
    Schedule,
    Shift, Slot, Revision,
    Interval,
    Staff,
)
from watcher.crud.base import (
    get_object_by_model,
    get_object_by_model_or_404,
    commit_object,
    _bulk_insert_refs,
    _set_fields,
    update_many_to_many_for_field,
)
from watcher.crud.shift import query_all_shifts_by_schedule
from watcher.logic.timezone import now


def get_schedule_by_id(db: Session, schedule_id: int) -> Schedule:
    return get_object_by_model(db=db, model=Schedule, object_id=schedule_id)


def get_schedule_or_404(db: Session, schedule_id: int) -> Schedule:
    return get_object_by_model_or_404(db=db, model=Schedule, object_id=schedule_id)


def query_active_schedules(db: Session) -> Query:
    return db.query(Schedule).filter(
        Schedule.state == enums.ScheduleState.active
    )


def query_active_schedules_with_active_revisions(db: Session):
    return query_active_schedules(db=db).filter(
        db.query(Revision.id)
        .filter(
            Revision.schedule_id == Schedule.id,
            Revision.state == enums.RevisionState.active,
        )
        .exists()
    )


def query_schedules_by_group(db: Session, schedules_group_id: int, query: Optional[Query] = None) -> Query:
    if query is None:
        query = db.query(Schedule)

    return query.filter(Schedule.schedules_group_id == schedules_group_id)


def query_schedules_without_allocation(session: Session, schedules_group_id: int) -> Query:
    """ Возвращаем расписания, у которого есть шифты, но ни у одного шифта нет человека """

    return (
        session.query(Schedule, Revision)
        .join(Revision, Revision.schedule_id == Schedule.id)
        .join(Shift, Shift.schedule_id == Schedule.id)
        .filter(
            Schedule.schedules_group_id == schedules_group_id,
            Revision.state != enums.RevisionState.disabled,
        )
        .group_by(Schedule.id, Revision.id)
        .having(func.count(Shift.staff_id) == 0)
        .order_by(Revision.apply_datetime)
    )


def query_schedules_by_composition(db: Session, service_id: int, composition_id: int) -> Query:
    """
    Возвращает расписания, в которых есть переданная композиция привязанная
    к активным ревизиям
    """
    return (
        db.query(Schedule)
        .join(Revision, Revision.schedule_id == Schedule.id)
        .join(Interval, Interval.revision_id == Revision.id)
        .join(Slot, Slot.interval_id == Interval.id)
        .filter(
            Revision.state == enums.RevisionState.active,
            Schedule.service_id == service_id,
            Slot.composition_id == composition_id,
        )
    )


def save_schedule_shifts_boundary_recalculation_error(
    db: Session,
    schedule_id: int,
    message: str,
    raise_exception: Optional[Callable] = None
) -> None:
    error = {
        'message': message,
        'datetime': now().strftime('%Y-%m-%d %H:%M:%S %Z'),
    }
    schedule = get_schedule_or_404(db, schedule_id)
    schedule.shifts_boundary_recalculation_error = error

    if raise_exception:
        raise raise_exception(message=message)


def schedule_has_shifts(db: Session, schedule_id: int) -> bool:
    return db.query(
        query_all_shifts_by_schedule(db, schedule_id).exists()
    ).scalar()


def query_schedules_by_service(db: Session, service_id: int, query: Optional[Query] = None) -> Query:
    if not query:
        query = db.query(Schedule)

    return query.filter(Schedule.service_id == service_id)


def create_schedule(db: Session, schedule: ScheduleCreateSchema, author: Staff) -> Schedule:
    data = schedule.dict()
    data.pop('start', None)
    show_in_services = data.pop('show_in_services', None)
    obj = Schedule(**data, author_id=author.id, responsibles=[author])

    if obj.schedules_group_id is None:
        obj.schedules_group = create_autogenerated_group_object()

    db_obj = commit_object(db=db, obj=obj)
    if show_in_services:
        _bulk_insert_refs(
            db=db, obj=db_obj,
            field_key='service_id',
            to_add=show_in_services,
            table=ScheduleShowInServices,
            related_field='schedule_id',
        )
    return db_obj


def patch_schedule(db: Session, obj: Schedule, schema: ScheduleWithValidatorSchema) -> Schedule:
    data = schema.dict(exclude_unset=True)
    show_in_services = data.pop('show_in_services', None)
    if data.get('schedules_group_id', False) is None:
        group = commit_object(
            db=db,
            obj=create_autogenerated_group_object(),
        )
        schedules_group_id = group.id
        data['schedules_group_id'] = schedules_group_id

    has_changes = _set_fields(obj=obj, update_data=data)
    if show_in_services is not None:
        current_refs = db.query(ScheduleShowInServices).filter(
            ScheduleShowInServices.schedule_id == obj.id
        )
        current_values = {ref.service_id for ref in current_refs}

        update_many_to_many_for_field(
            db=db, current=current_values,
            target=show_in_services,
            table=ScheduleShowInServices,
            current_refs=current_refs,
            field_key='service_id', obj=obj,
            related_field='schedule_id',
        )
    if has_changes or show_in_services is not None:
        obj = commit_object(db=db, obj=obj)

    return obj


def set_recalculation(session: Session, obj_ids: Iterable[int], target: bool) -> None:
    session.query(Schedule).filter(
        Schedule.id.in_(obj_ids)
    ).update(
        {Schedule.recalculation_in_process: target},
        synchronize_session=False
    )
