import datetime
import logging
from collections import defaultdict
from dateutil.parser import parse

from sqlalchemy.orm import Session, joinedload
from watcher import enums
from watcher.db import (
    AbcMigration,
    Composition,
    Schedule,
    Interval,
    Revision,
    Slot,
    Shift,
)
from watcher.db.base import dbconnect
from watcher.crud.revision import query_active_revisions
from watcher.crud.role import get_role_by_id
from watcher.crud.staff import get_staff_by_id
from watcher.crud.interval import query_intervals_by_schedule
from watcher.crud.slot import query_slots_by_schedule
from watcher.logic.clients.abc import abc_client
from watcher.tasks.composition import update_composition
from watcher.logic.exceptions import BaseWatcherException, AbcMigrationException
from watcher.logic.schedule_group import create_autogenerated_group_object
from watcher.logic.timezone import now
from watcher.tasks.base import lock_task
from watcher.tasks.generating_shifts import proceed_new_shifts

logger = logging.getLogger(__name__)


def create_migrations(session: Session, service_id: int, author_id: int):
    logger.info(f'Creating migration records for service: {service_id}')
    abc_schedules = abc_client.get_schedules(service_id=service_id, fields='id,slug,consider_other_schedules')
    exist_migrations = {
        migration.abc_schedule_id for migration in
        session.query(AbcMigration).filter(AbcMigration.service_id == service_id).all()
    }
    new_abc_schedule_ids = []
    for abc_schedule in abc_schedules:
        abc_schedule_id = abc_schedule['id']
        if abc_schedule_id in exist_migrations:
            logger.info(f'Migration records exists for service: {service_id}, abc_schedule_id: {abc_schedule_id}')
            continue
        new_migration_record = AbcMigration(
            service_id=service_id,
            abc_schedule_id=abc_schedule_id,
            author_id=author_id,
            consider_other_schedules=abc_schedule['consider_other_schedules']
        )
        session.add(new_migration_record)
        new_abc_schedule_ids.append(abc_schedule_id)
        logger.info(f'Migration records created for service: {service_id}, abc_schedule_id: {abc_schedule_id}')
    session.commit()
    for abc_schedule_id in new_abc_schedule_ids:
        prepare_migration_task.delay(service_id=service_id, abc_schedule_id=abc_schedule_id)


@lock_task(lock_key=lambda service_id, abc_schedule_id, *args, **kwargs: 'migrate_abc_schedule_{}'.format(abc_schedule_id))
@dbconnect
def prepare_migration_task(session: Session, service_id: int, abc_schedule_id: int):
    migration = session.query(AbcMigration).filter(
        AbcMigration.service_id == service_id,
        AbcMigration.abc_schedule_id == abc_schedule_id,
    ).first()
    if migration is None:
        logger.error(f'No migration found for abc_schedule_id: {abc_schedule_id}')
        return
    if migration.status != enums.AbcMigrationStatus.created:
        logger.info(f'Skipping migration prepare for abc_schedule_id: {abc_schedule_id}, in status {migration.status}')
    else:
        migration.status = enums.AbcMigrationStatus.preparing
        session.commit()
        try:
            logger.info(f'Migration prepare started for abc_schedule_id: {abc_schedule_id}')
            prepare_migration(session=session, abc_migration=migration)
        except BaseWatcherException as exc:
            logger.error(f'Migration prepare failed for abc_schedule_id: {abc_schedule_id}')
            migration.status = enums.AbcMigrationStatus.preparing_fail
            migration.migration_error = exc.to_json()
        else:
            migration.status = enums.AbcMigrationStatus.prepared
            logger.info(f'Migration prepare finished for abc_schedule_id: {abc_schedule_id}')


def finalize_migrations(session: Session, service_id: int):
    logger.info(f'Finalizing migration for service: {service_id}')
    exist_migrations = {
        migration.abc_schedule_id for migration in
        session.query(AbcMigration).filter(AbcMigration.service_id == service_id).all()
    }
    for abc_schedule_id in exist_migrations:
        finalize_migration_task.delay(service_id=service_id, abc_schedule_id=abc_schedule_id)


@lock_task(lock_key=lambda service_id, abc_schedule_id, *args, **kwargs: 'finalize_abc_schedule_{}'.format(abc_schedule_id))
@dbconnect
def finalize_migration_task(session: Session, service_id: int, abc_schedule_id: int):
    migration = session.query(AbcMigration).filter(
        AbcMigration.service_id == service_id,
        AbcMigration.abc_schedule_id == abc_schedule_id,
    ).first()
    if migration is None:
        logger.error(f'No migration found for abc_schedule_id: {abc_schedule_id}')
        return
    if migration.status != enums.AbcMigrationStatus.prepared:
        logger.error(f'Migration is not prepared for abc_schedule_id: {abc_schedule_id}, in status {migration.status}')
        return
    migration.status = enums.AbcMigrationStatus.finalizing
    session.commit()
    try:
        logger.info(f'Migration finalization started for abc_schedule_id: {abc_schedule_id}')
        finalize_migration(session=session, abc_migration=migration)
    except BaseWatcherException as exc:
        logger.error(f'Migration finalization failed for abc_schedule_id: {abc_schedule_id}')
        migration.status = enums.AbcMigrationStatus.finalizing_fail
        migration.migration_error = exc.to_json()
    else:
        migration.status = enums.AbcMigrationStatus.finished
        logger.info(f'Migration finalization finished for abc_schedule_id: {abc_schedule_id}')


def find_migration_schedule_group(session: Session, service_id: int):
    migration_with_common_group = (
        session.query(AbcMigration)
        .options(
            joinedload(AbcMigration.schedule).joinedload(Schedule.schedules_group)
        )
        .filter(
            AbcMigration.service_id == service_id,
            AbcMigration.schedule_id.isnot(None),
            AbcMigration.consider_other_schedules.is_(True),
        )
    ).first()
    if migration_with_common_group:
        return migration_with_common_group.schedule.schedules_group
    else:
        return None


def get_abc_shifts(abc_schedule_id: int):
    abc_shifts = abc_client.get_shifts(schedule_id=abc_schedule_id)
    if len(abc_shifts) == 0:
        raise AbcMigrationException(message={
            'ru': 'Отсутствуют смены для переноса',
            'en': 'No shifts to migrate',
        })
    # Интересуют смены в прошлом, и подтвержденные смены в будущем
    now_time = now()
    cut_index = 0
    for i, abc_shift in enumerate(abc_shifts):
        abc_shift['start_datetime'] = parse(abc_shift['start_datetime'])
        abc_shift['end_datetime'] = parse(abc_shift['end_datetime'])
        if abc_shift['is_approved'] or abc_shift['start_datetime'] <= now_time:
            cut_index = i
    return abc_shifts[:cut_index + 1]


def get_or_create_schedule(session: Session, abc_migration: AbcMigration, abc_schedule_info: dict):
    if abc_migration.schedule_id:
        schedule = abc_migration.schedule
    else:
        schedule_group = None
        if abc_migration.consider_other_schedules:
            schedule_group = find_migration_schedule_group(session=session, service_id=abc_migration.service_id)
        if not schedule_group:
            schedule_group = create_autogenerated_group_object()

        schedule = Schedule(
            slug=f'new-{abc_schedule_info["slug"]}',
            name=abc_schedule_info['name'],
            description=abc_schedule_info['description'],
            service_id=abc_migration.service_id,
            is_important=abc_schedule_info['is_important'],
            days_for_notify_of_problems=None,
            days_for_notify_of_begin=None,
            pin_shifts=abc_schedule_info['autoapprove_timedelta'],
            recalculate=abc_schedule_info['recalculate'],
            author_id=abc_migration.author_id,
            schedules_group=schedule_group,
        )
        abc_migration.schedule = schedule
        session.add(schedule)
        session.commit()

    return schedule


def get_or_create_composition(session: Session, schedule: Schedule, abc_migration: AbcMigration,
                              abc_schedule_info: dict):
    # создаем отдельный пул для каждого расписания
    composition_slug = f'migration-{abc_schedule_info["slug"]}'
    composition = session.query(Composition).filter(
        Composition.service_id == abc_migration.service_id,
        Composition.slug == composition_slug,
    ).first()

    if not composition:
        composition = Composition(
            service_id=abc_migration.service_id,
            slug=composition_slug,
            name=schedule.name,
        )
        if abc_schedule_info['algorithm'] == 'manual_order':
            composition.roles = [get_role_by_id(db=session, role_id=abc_schedule_info['role']['id'])]
            composition.participants = [
                get_staff_by_id(db=session, staff_id=order['person']['abc_id'])
                for order in abc_schedule_info['orders'] if order['person']
            ]
            composition.autoupdate = False
        elif abc_schedule_info['role']:
            composition.roles = [get_role_by_id(db=session, role_id=abc_schedule_info['role']['id'])]
        else:
            # Роль не указана - значит дежурит весь сервис
            composition.full_service = True

        session.add(composition)
        session.commit()
        update_composition(
            composition_id=composition.id,
            force=True,
            _lock=False,
        )
        session.refresh(composition)

    return composition


def get_or_create_interval(session: Session, schedule: Schedule, abc_schedule_info: dict, abc_shifts: list):
    interval = query_intervals_by_schedule(db=session, schedule_id=schedule.id).first()
    if not interval:
        revision = query_active_revisions(db=session, schedule_id=schedule.id).first()
        if not revision:
            revision = Revision(
                schedule_id=schedule.id,
                apply_datetime=abc_shifts[0]['start_datetime'],
            )
            session.add(revision)

        interval = Interval(
            revision=revision,
            order=1,
            name='Основной',
            schedule_id=schedule.id,
            duration=abc_schedule_info['duration'],
            unexpected_holidays=(
                enums.IntervalUnexpectedHolidays.ignoring if abc_schedule_info['duty_on_holidays']
                else enums.IntervalUnexpectedHolidays.remove
            ),
            weekend_behaviour=(
                enums.IntervalWeekendsBehaviour.ignoring if abc_schedule_info['duty_on_weekends']
                else enums.IntervalWeekendsBehaviour.extend
            ),
        )
        session.add(interval)
        session.commit()

    return interval


def get_or_create_slots(session: Session, schedule: Schedule, interval: Interval, composition: Composition,
                        abc_schedule_info: dict):
    slots = query_slots_by_schedule(db=session, schedule_id=schedule.id).all()
    if len(slots) != abc_schedule_info['persons_count']:
        # Создаем недостающее количество слотов
        slot_count_to_create = abc_schedule_info['persons_count'] - len(slots)
        for _ in range(slot_count_to_create):
            new_slot = Slot(
                interval_id=interval.id,
                composition_id=composition.id,
                show_in_staff=False,
            )
            session.add(new_slot)
            slots.append(new_slot)
        session.commit()

    return slots


def recreate_shifts(session: Session, schedule: Schedule, slots: list, abc_shifts: list):
    # Удаляем все существующие шифты
    shifts_query = session.query(Shift).filter(Shift.schedule_id == schedule.id)
    shifts_query.delete(synchronize_session=False)
    session.commit()

    prev_shift = None
    slot_counter = defaultdict(int)
    for abc_shift in abc_shifts:
        abc_start = abc_shift['start_datetime']
        abc_end = abc_shift['end_datetime']
        current_slot = slots[slot_counter[abc_start]]
        new_shift = Shift(
            slot_id=current_slot.id,
            schedule_id=schedule.id,
            start=abc_start,
            end=abc_end,
            staff_id=abc_shift['person']['abc_id'] if abc_shift['person'] else None,
            approved=abc_shift['is_approved'],
        )
        new_shift.prev = prev_shift
        prev_shift = new_shift
        slot_counter[abc_start] += 1
        session.add(new_shift)
    session.commit()


def prepare_migration(session: Session, abc_migration: AbcMigration):
    """
    мигрирует одно расписание из abc
    """
    abc_schedule_id = abc_migration.abc_schedule_id

    schedule_fields = (
        'id,slug,name,description,days_for_problem_notification,days_for_begin_shift_notification,is_important,'
        'autoapprove_timedelta,recalculate,algorithm,orders,duration,duty_on_holidays,duty_on_weekends,persons_count,'
        'role,role_on_duty,show_in_staff'
    )
    abc_schedule_info = abc_client.get_schedule(schedule_id=abc_schedule_id, fields=schedule_fields)

    abc_shifts = get_abc_shifts(abc_schedule_id=abc_schedule_id)

    schedule = get_or_create_schedule(
        session=session,
        abc_migration=abc_migration,
        abc_schedule_info=abc_schedule_info,
    )

    composition = get_or_create_composition(
        session=session,
        schedule=schedule,
        abc_migration=abc_migration,
        abc_schedule_info=abc_schedule_info,
    )

    interval = get_or_create_interval(
        session=session,
        schedule=schedule,
        abc_schedule_info=abc_schedule_info,
        abc_shifts=abc_shifts,
    )

    slots = get_or_create_slots(
        session=session,
        schedule=schedule,
        interval=interval,
        composition=composition,
        abc_schedule_info=abc_schedule_info,
    )

    recreate_shifts(
        session=session,
        schedule=schedule,
        slots=slots,
        abc_shifts=abc_shifts,
    )
    # достраиваем остальные смены
    proceed_new_shifts.delay(schedule_id=schedule.id)

    abc_client.create_duty_to_watcher_link(
        abc_id=abc_schedule_id,
        watcher_id=schedule.id,
    )


def finalize_migration(session: Session, abc_migration: AbcMigration):

    abc_schedule_id = abc_migration.abc_schedule_id

    schedule_fields = (
        'id,slug,days_for_problem_notification,days_for_begin_shift_notification,show_in_staff,role_on_duty'
    )
    abc_schedule_info = abc_client.get_schedule(schedule_id=abc_schedule_id, fields=schedule_fields)
    if not abc_schedule_info['slug'].startswith('deleted-'):
        data = {'slug': f'deleted-{abc_schedule_info["slug"]}'}
        abc_client.patch_schedule(schedule_id=abc_schedule_id, data=data)

    schedule = abc_migration.schedule
    schedule.slug = schedule.slug.removeprefix('new-')
    schedule.days_for_notify_of_problems = datetime.timedelta(days=abc_schedule_info['days_for_problem_notification']),
    schedule.days_for_notify_of_begin = [
        datetime.timedelta(days=days) for days in
        abc_schedule_info['days_for_begin_shift_notification']
    ]
    slots = query_slots_by_schedule(db=session, schedule_id=schedule.id).all()
    for slot in slots:
        slot.role_on_duty_id = abc_schedule_info['role_on_duty'],
        slot.show_in_staff = abc_schedule_info['show_in_staff']
    session.commit()

    abc_client.delete_schedule(schedule_id=abc_schedule_id)
