import datetime
import math
import time
import logging

from dateutil.parser import parse
from typing import Optional, Iterable, Union
from collections import defaultdict

from sqlalchemy.orm import (
    Session,
    joinedload,
)

from watcher.config import settings
from watcher.db import (
    Event,
    Gap,
    ManualGap,
    ManualGapSettings,
    Shift,
)
from watcher.db.base import dbconnect
from watcher.tasks.base import lock_task
from watcher.logic.holidays import (
    is_weekend,
    is_holiday,
)
from watcher.logic.timezone import now
from watcher import enums

logger = logging.getLogger(__name__)


def _get_hours(
    date_from: datetime.datetime,
    date_to: datetime.datetime,
    obj: Union[Shift, Gap, ManualGap],
) -> float:
    date_from = max(date_from, obj.start)
    date_to = min(date_to, obj.end)

    diff = (date_to - date_from)
    days, seconds = diff.days, diff.seconds
    return days * 24 + math.ceil(seconds / 3600)


def _run_yql(table: str, columns: tuple, items: Iterable, column_types: tuple = None, truncate: bool = True) -> None:
    from yql.api.v1.client import YqlClient

    yql_client = YqlClient(token=settings.YQL_OAUTH_TOKEN)

    query = [
        'USE hahn;',
        'PRAGMA yt.TmpFolder = "//home/abc/tmp";',
        'PRAGMA yt.QueryCacheMode = "disable";',
        'INSERT INTO',
        f'`{table}` {"with truncate" if truncate else ""} ',
        f'({",".join(columns)})',
        'VALUES'
    ]

    value_rows = []
    for i, item in enumerate(items):
        value_row = []
        for value in item:
            if value is None:
                value_row.append('null')
            elif isinstance(value, int):
                value_row.append(str(value))
            else:
                value_row.append('"{}"'.format(str(value).replace('"', '\\"')))
        if i == 0 and column_types:
            # достаточно явно сделать CAST первой строки данных, чтобы явно задать типы
            value_row = [f'CAST({value} AS {column_types[j]})' for j, value in enumerate(value_row)]
        value_rows.append(f'({", ".join(value_row)})')
    query.append(', '.join(value_rows))
    request = yql_client.query(' '.join(query))
    request.run()

    while request.status not in ('COMPLETED', 'ERROR'):
        logger.debug(f'Export Duty data to Yql, request in status {request.status}')
        time.sleep(1.0)
    if request.status == 'ERROR':
        errors = '; '.join([error.format_issue() for error in request.errors])
        logger.error(f'Error occurred on duty data export: [{errors}]')
        raise Exception('Upload duty data failed')


@lock_task
@dbconnect
def upload_duty_to_yt(session: Session, for_date: Optional[str] = None):
    for_date = parse(for_date).date() if for_date else datetime.date.today() - datetime.timedelta(days=1)
    for_date_min = settings.DEFAULT_TIMEZONE.localize(
        datetime.datetime.combine(for_date, datetime.datetime.min.time())
    )
    for_date_max = settings.DEFAULT_TIMEZONE.localize(
        datetime.datetime.combine(for_date, datetime.datetime.max.time())
    )
    weekend = is_weekend(date=for_date)
    holiday = False
    if not weekend and is_holiday(session=session, date=for_date):
        holiday = True

    shifts = session.query(Shift).filter(
        for_date_min < Shift.end,
        for_date_max > Shift.start,
        Shift.staff_id.isnot(None),
        ~Shift.sub_shifts.any(),
    ).options(
        joinedload(Shift.staff),
        joinedload(Shift.schedule),
        joinedload(Shift.slot),
    )

    items = set()
    for shift in shifts:
        hours = _get_hours(
            date_from=for_date_min,
            date_to=for_date_max,
            obj=shift
        )
        items.add(
            (
                shift.schedule.service.slug,
                shift.schedule.slug,
                shift.schedule.service_id,
                shift.schedule_id,
                shift.staff.login,
                shift.staff_id,
                shift.is_primary,
                weekend,
                holiday,
                hours,
                for_date.isoformat()
            )
        )
    if items:
        columns = (
            'service_slug',
            'schedule_slug',
            'service_id',
            'schedule_id',
            'staff_login',
            'staff_id',
            'is_primary',
            'is_weekend',
            'is_holiday',
            'duty_hours',
            'current_date',
        )
        _run_yql(columns=columns, table=f'home/abc/duty2/shifts/{for_date.isoformat()}', items=items)

    # загрузим теперь данные гепов
    for_date_min_utc = settings.UTC.localize(
        datetime.datetime.combine(for_date, datetime.datetime.min.time())
    )
    for_date_max_utc = settings.UTC.localize(
        datetime.datetime.combine(for_date, datetime.datetime.max.time())
    )
    gaps_map = defaultdict(list)
    gaps = session.query(Gap).filter(
        for_date_min_utc < Gap.end,
        for_date_max_utc > Gap.start,
        Gap.status == enums.GapStatus.active,
        ~Gap.work_in_absence,
    ).options(
        joinedload(Gap.staff),
    )
    for gap in gaps:
        gaps_map[gap.staff_id].append(gap)

    gaps_items = set()
    for staff_id, gaps in gaps_map.items():
        hours = 0
        for gap in gaps:
            if gap.full_day:
                hours += 24
            else:
                hours += _get_hours(
                    date_from=for_date_min,
                    date_to=for_date_max,
                    obj=gap,
                )
        if hours > 0:
            gaps_items.add(
                (
                    gap.staff.login,
                    gap.staff_id,
                    weekend,
                    holiday,
                    min(hours, 24),
                    for_date.isoformat(),
                )
            )
    if gaps_items:
        columns = (
            'staff_login',
            'staff_id',
            'is_weekend',
            'is_holiday',
            'gap_hours',
            'current_date',
        )
        _run_yql(columns=columns, table=f'home/abc/duty2/gaps/{for_date.isoformat()}', items=gaps_items)

    # и ручных гепов
    manual_gaps = session.query(ManualGap).filter(
        for_date_min < ManualGap.end,
        for_date_max > ManualGap.start,
        ManualGap.is_active.is_(True),
    ).options(
        joinedload(ManualGap.staff),
        joinedload(ManualGap.gap_settings),
        joinedload(ManualGap.gap_settings).joinedload(ManualGapSettings.schedules),
        joinedload(ManualGap.gap_settings).joinedload(ManualGapSettings.services),
    ).order_by(ManualGap.staff_id)
    manual_gaps_items = set()
    for gap in manual_gaps:
        hours = _get_hours(
            date_from=for_date_min,
            date_to=for_date_max,
            obj=gap,
        )

        manual_gaps_items.add(
            (
                gap.staff.login,
                gap.staff_id,
                gap.gap_settings.all_services,
                tuple(service.id for service in gap.gap_settings.services),
                tuple(schedule.id for schedule in gap.gap_settings.schedules),
                weekend,
                holiday,
                hours,
                for_date.isoformat(),
            )
        )
    if manual_gaps_items:
        columns = (
            'staff_login',
            'staff_id',
            'all_services',
            'services',
            'schedules',
            'is_weekend',
            'is_holiday',
            'gap_hours',
            'current_date',
        )
        _run_yql(columns=columns, table=f'home/abc/duty2/manual_gaps/{for_date.isoformat()}', items=manual_gaps_items)


@lock_task
def upload_duty_future():
    """
    загружаем данные по дежурствам в YT на будущие даты
    нужно аналитикам для примерного рассчета
    """
    start = datetime.date.today()
    for delta in range(1, 180):
        upload_duty_to_yt(
            for_date=(start + datetime.timedelta(days=delta)).isoformat()
        )


@lock_task
@dbconnect
def dump_events_to_yt(session: Session):
    """
    выгружаем обработанные данные в YT, после этого удаляем записи в базе
    """
    processed_events = session.query(Event).filter(
        Event.updated_at < now() - datetime.timedelta(days=1),
        Event.state == enums.EventState.processed,
    ).all()

    event_map = defaultdict(list)
    for event in processed_events:
        event_map[event.created_at.strftime("%Y-%m")].append(event)

    columns = (
        'created_at',
        'updated_at',
        'id',
        'table',
        'kind',
        'obj_id',
        'source',
        'type',
        'state',
        'object_data',
        'old_keys',
        'remote_modified_at',
    )
    column_types = (
        'String?',
        'String?',
        'Int64?',
        'String?',
        'String?',
        'Int64?',
        'String?',
        'String?',
        'String?',
        'String?',
        'String?',
        'String?',
    )
    for date_str, events in event_map.items():
        ids_to_delete = []
        data = []
        for event in events:
            ids_to_delete.append(event.id)
            data.append(
                (
                    event.created_at.isoformat() if event.created_at else None,
                    event.updated_at.isoformat() if event.updated_at else None,
                    event.id,
                    event.table,
                    event.kind,
                    event.obj_id,
                    event.source,
                    event.type,
                    event.state,
                    event.object_data,
                    event.old_keys,
                    event.remote_modified_at.isoformat() if event.remote_modified_at else None,
                )
            )
            # пишем порционно, иначе возникает ошибка 'Entity too large'
            if len(data) >= settings.YT_DUMP_EVENTS_MAX_PORTION:
                _run_yql(columns=columns, table=f'home/abc/duty2/event/{date_str}', items=data,
                         column_types=column_types, truncate=False)
                session.query(Event).filter(Event.id.in_(ids_to_delete)).delete(synchronize_session=False)
                ids_to_delete = []
                data = []
        if data:
            _run_yql(columns=columns, table=f'home/abc/duty2/event/{date_str}', items=data,
                     column_types=column_types, truncate=False)
            session.query(Event).filter(Event.id.in_(ids_to_delete)).delete(synchronize_session=False)
