import logging

from collections import defaultdict
from typing import List, Tuple, Iterable, Optional

from sqlalchemy import func
from sqlalchemy.orm import Session

from watcher.celery_app import app
from watcher.crud.problem import (
    query_active_problems,
    query_shifts_without_problems,
    query_problems_by_schedule,
)
from watcher.crud.gap import query_true_gaps
from watcher.logic.timezone import now, localize
from watcher.logic.problem import resolve_problem, is_resolved
from watcher.logic.people_allocation import (
    check_participant_can_duty,
    find_intersecting_gap,
)
from watcher.db.base import dbconnect
from watcher.db import Staff, Shift, Gap, Schedule, ManualGap, Problem
from watcher.enums import ProblemReason, GapStatus

from .people_allocation import start_people_allocation
from .base import lock_task

logger = logging.getLogger(__name__)


def _get_ref_for_staff_has_gap_shift(
    db: Session,
    staff_ids: Iterable[int],
    only_shifts_without_problems: Optional[bool] = True,
) -> List[Tuple[Staff, Shift, Gap | ManualGap]]:
    current_now = now()
    # достаем самый передний край начала интересующих нас шифтов, для определения нужных гэпов
    min_shift_start = (
        db.query(func.min(Shift.start))
        .filter(
            Shift.staff_id.in_(staff_ids),
            Shift.end >= current_now,
        )
    ).one()
    min_shift_start = min_shift_start[0]  # Возвращается Row(min_shift_start, )
    if not min_shift_start:
        return []

    models = [Gap, ManualGap]
    queries = {}
    for gap_model in models:
        # TODO: как то переписать gap_table.end - gap_table.start
        queries[gap_model] = (
            db.query(Staff, Shift, gap_model)
            .join(Shift, Shift.staff_id == Staff.id)
            .join(Schedule, Schedule.id == Shift.schedule_id)
            .join(gap_model, gap_model.staff_id == Staff.id)
            .filter(
                Staff.id.in_(staff_ids),
                Shift.end >= current_now,
                gap_model.end >= min_shift_start,
                gap_model.end - gap_model.start >= Schedule.length_of_absences,
            )
        )

    duty_gap_query = query_true_gaps(db=db, query=queries[Gap])
    manual_gap_query = queries[ManualGap].filter(ManualGap.is_active.is_(True))

    staff_shifts = []
    if only_shifts_without_problems:
        duty_gap_query = query_shifts_without_problems(db=db, query=duty_gap_query)
        manual_gap_query = query_shifts_without_problems(db=db, query=manual_gap_query)

    staff_shifts.extend(duty_gap_query)
    staff_shifts.extend(manual_gap_query)

    return staff_shifts


@app.task
@dbconnect
def create_problems_for_staff_has_gap_shifts(
    session: Session, staff_ids: List[int],
    need_people_allocation: Optional[bool] = True,
):
    """
    Создаем проблемы для смен, если запланированный дежурный уже не может продежурить

    :param staff_ids: сотрудники для которых нужно проверить, могут ли они продежурить
    :return:
    """
    staff_shifts = _get_ref_for_staff_has_gap_shift(session, staff_ids)

    if not staff_shifts:
        return

    participant_gaps = defaultdict(list)
    shifts_to_review = set()

    duty_gap_ids = set()
    manual_gap_ids = set()
    for staff, shift, gap in staff_shifts:
        shifts_to_review.add(shift)
        if isinstance(gap, ManualGap) and gap.id not in manual_gap_ids:
            manual_gap_ids.add(gap.id)
            participant_gaps[staff.id].append(gap)
        elif isinstance(gap, Gap) and gap.id not in duty_gap_ids:
            duty_gap_ids.add(gap.id)
            participant_gaps[staff.id].append(gap)

    problems = []
    groups_to_relocate = defaultdict(list)

    for shift in shifts_to_review:
        problem_gap = find_intersecting_gap(
            shift=shift, participant_gaps=participant_gaps[shift.staff_id]
        )
        if problem_gap is None:
            continue

        if not shift.approved:
            shift.staff_id = None
            groups_to_relocate[shift.schedule.schedules_group_id].append(localize(shift.start))
        else:
            logger.info(
                f'Creating problem staff_has_gap for shift: {shift.id} '
                f'from create_problems_for_staff_has_gap_shifts'
            )
            if problem_gap:
                type_to_name = {Gap: 'duty_gap_id', ManualGap: 'manual_gap_id'}
                gap_id_attr_name = type_to_name[type(problem_gap)]
                problems.append({
                    'shift_id': shift.id,
                    'staff_id': shift.staff_id,
                    'reason': ProblemReason.staff_has_gap,
                    gap_id_attr_name: problem_gap.id,
                })
            else:
                logger.warning(
                    f'Gap is not found for problem staff_has_gap for shift: {shift.id} '
                )

    if problems:
        session.bulk_insert_mappings(Problem, problems)
    if need_people_allocation:
        for group_id, dates in groups_to_relocate.items():
            logger.info(f'Starting people allocation for {group_id}')
            start_people_allocation.delay(
                schedules_group_id=group_id,
                start_date=min(dates),
            )


@lock_task(save_metrics=True, send_to_unistat=True)
@dbconnect
def resolve_shifts_problems(session: Session, schedule_id: Optional[int] = None):
    """
    Если возможно - переводим проблемы в статус "решено"

    :param schedule_id: если не передано - рассмотриваем все шифты всех расписаний
    """
    logger.info(f'Processing resolve_shifts_problems for schedule: {schedule_id}')

    # TODO: добавить фильтрацию по двум причинам
    problems = query_active_problems(session)

    if schedule_id:
        problems = query_problems_by_schedule(session, schedule_id, query=problems)

    # Если поменяли дежурного - проблема nobody_on_duty, либо staff_has_gap решена
    for problem in problems:
        if problem.staff_id != problem.shift.staff_id:
            resolve_problem(problem)

    # если дежурного не поменяли - попробуем узнать появилось ли свободное время у текущих дежурных
    problems = list(filter(lambda x: not is_resolved(x), problems))

    staff_ids = set([problem.staff_id for problem in problems if problem.staff_id])
    staff_gaps = _get_ref_for_staff_has_gap_shift(
        db=session, staff_ids=staff_ids, only_shifts_without_problems=False
    )

    participant_gaps = defaultdict(list)
    for staff, _, gap in staff_gaps:
        participant_gaps[staff.id].append(gap)

    groups_to_relocate = defaultdict(list)

    current_now = now()
    for problem in problems:
        if problem.reason == ProblemReason.staff_has_gap:
            # Решаем проблемы staff_has_gap в прошлом:
            if problem.shift.end < current_now:
                resolve_problem(problem)
            # Если дежурный уже может дежурить - проблема решена
            elif check_participant_can_duty(
                shifts=[problem.shift],
                participant_gaps=participant_gaps[problem.staff_id],
            ):
                resolve_problem(problem)
        elif problem.reason == ProblemReason.nobody_on_duty:
            if (
                # в коде сейчас такое условие никогда не выполняется, проблемы эти всегда без
                # привязки к гепам создаются
                (problem.duty_gap_id and problem.duty_gap.status == GapStatus.deleted)
                or (problem.manual_gap_id and not problem.manual_gap.is_actve)
            ):
                # нужно запустить пересчет, возможно получится найти дежурного
                groups_to_relocate[problem.shift.schedule.schedules_group_id].append(problem.shift.start)
                resolve_problem(problem)

    for group_id, dates in groups_to_relocate.items():
        logger.info(f'Starting people allocation for {group_id} from resolve_shifts_problems')
        start_people_allocation.delay(
            schedules_group_id=group_id,
            start_date=min(dates),
        )
