from datetime import datetime, timedelta
from dateutil import parser
import json
import logging
from typing import List

from pymongo import MongoClient
from django_replicated.decorators import use_master

from django.apps import apps
from django.conf import settings

from staff.emission.django.emission_master.models import MasterLog
from staff.groups.models import GroupMembership
from staff.lib.db import atomic
from staff.lib.decorators import responding_json
from staff.lib.requests import Session
from staff.monitorings.models import FixedEmissionEvents


_staffapi_groups_url = (
    'https://{host}/v3/groups?_fields=ancestors.id,id&type=department&_limit=999999'.format(
        host=settings.STAFFAPI_HOST,
    )
)
_staffapi_memberships_url = (
    'https://{host}/v3/groupmembership'
    '?_fields=id,group.id,group.url,person.id,person.login&_limit=99999'.format(
        host=settings.STAFFAPI_HOST
    )

)

logger = logging.getLogger('monitoring.staff_api')
session = Session()
session.headers['Authorization'] = 'OAuth {token}'.format(token=settings.ROBOT_STAFF_OAUTH_TOKEN)

# VIEWS


@use_master
@responding_json
def check_staffapi_consistency(request):
    """
    Сравниваем id посланных и обработанных записей в стафф-апи и выводим разницу с ссылкой на починку
    """
    from_date = request.GET.get('from_date')
    if from_date:
        from_date = parser.parse(from_date)
    else:
        from_date = datetime.today() - timedelta(days=1)

    difference = _get_difference_between_staff_and_api(from_date)

    if request.GET.get('fix-it-please', False):
        _fix_staffapi_by_events_ids(difference)

    if not difference:
        return {}
    else:
        return {
            'missed_events': difference,
            'magic_link': request.build_absolute_uri(
                '/_check/check_staffapi_consistency?from_date={}&fix-it-please=1'.format(from_date.isoformat())
            ),
        }


@responding_json
def check_group_consistency(request):
    """
    Проверка что иерархия департаментных групп не разломана
    Т.е. у группы и у родительской группы одинаковая цепочка
    родителей с учётом самой родительской группы
    """

    ancestors = _get_api_ancestors_data()

    problem_groups = set()
    problems = {}

    for dep, anc in ancestors.items():
        if not anc:
            continue

        if ancestors[anc[-1]] != anc[:-1]:
            problem_groups.add(dep)
            problem_groups.add(anc[-1])

            problems[dep] = {
                'chain': list(reversed(anc)),
                'parent_chain': list(ancestors[anc[-1]])
            }

    return problems


@responding_json
def check_memberships_actuality(request):
    """
    Проверка что групмембершипы в Стаффе и в СтафффАпи соответствуют друг другу.
    """
    try_to_fix = int(request.GET.get('fix', 0))

    staff_memberships = _get_staff_memberships_data()
    api_memberships = _get_api_memberships_data()

    problems = {}

    in_api_but_not_in_staff = set(api_memberships) - set(staff_memberships)
    in_staff_but_not_in_api = set(staff_memberships) - set(api_memberships)

    if in_api_but_not_in_staff or in_staff_but_not_in_api:
        problems['in_api_but_not_in_staff'] = [
            api_memberships[pk] for pk in in_api_but_not_in_staff
        ]
        problems['in_staff_but_not_in_api'] = [
            staff_memberships[pk] for pk in in_staff_but_not_in_api
        ]
        if try_to_fix:
            problems['__fix_logs__'] = _fix_memberships(
                memberships_to_add=in_staff_but_not_in_api,
                memberships_to_delete=in_api_but_not_in_staff,
            )

    return problems


def _fix_staffapi_by_events_ids(events_ids):
    logs = MasterLog.objects.filter(id__in=events_ids)
    for log in logs:
        data = json.loads(log.data)
        model_name, pk = data[0]['model'], data[0]['pk']
        model = apps.get_model(model_name)
        instance_to_save = model.objects.filter(pk=pk).first()

        if not instance_to_save:
            if log.action == 'delete':
                logger.warning('Trying to delete entity %s pk=%d from staffapi', model_name, pk)
                MasterLog.objects.create(data=log.data, action=log.action)
        else:
            # Department models require active transaction
            with atomic():
                instance_to_save.save()

        FixedEmissionEvents.objects.create(log=log)


def _get_difference_between_staff_and_api(from_date):
    handled_events = set(_get_staffapi_handled_events(from_date))

    events_from_masterlog = set(
        MasterLog.objects
        .filter(id__gte=min(handled_events), id__lte=max(handled_events))
        .exclude(id__in=FixedEmissionEvents.objects.values_list('log_id'))
        .values_list('id', flat=True)
    )

    difference = list(events_from_masterlog - handled_events)

    return difference


def _get_staffapi_handled_events(from_date):
    # type: (datetime) -> List[int]
    connection = MongoClient(settings.STAFFAPI_MONGO_URL, replicaSet=settings.MONGO_SET_NAME)
    db = connection['staffapi']
    return [event['_id'] for event in db['staff_event'].find({'time': {'$gte': from_date.isoformat()}}, {'id': 1})]


def _get_api_groups_data():
    response = session.get(_staffapi_groups_url)
    if response.status_code != 200:
        raise Exception('staff-api returned %s. body: %s', response.status_code, response.content)

    return json.loads(response.content)['result']


def _get_api_ancestors_data():
    groups_data = _get_api_groups_data()
    return {
        g['id']: [x['id'] for x in g['ancestors']]
        for g in groups_data
    }


def _get_api_memberships_data():

    api_memberships_data = session.get(_staffapi_memberships_url, timeout=20).json()['result']
    api_memberships_data = {ms['id']: ms for ms in api_memberships_data}
    return api_memberships_data


def _get_staff_memberships_data():
    staff_memberships_qs = (
        GroupMembership.objects
        .exclude(group__url='option')
        .values('id', 'staff_id', 'staff__login', 'group_id', 'group__url')
    )
    return {ms['id']: ms for ms in staff_memberships_qs}


def _fix_memberships(memberships_to_add, memberships_to_delete):
    """
    Пытаемся исправить данные в staff-api
    добавляя недостающие мембершипы и удаляя лишние
    """
    updated_memberships = _actualize_memberships_in_staff_api(memberships_to_add)
    deleted_memberships = _drop_memberships_from_staff_api(memberships_to_delete)
    return {
        'updated_memberships': updated_memberships,
        'deleted_memberships': deleted_memberships,
    }


def _actualize_memberships_in_staff_api(membership_ids):
    logs = []
    for membership in GroupMembership.objects.filter(id__in=membership_ids):
        membership.save()
        logs.append(membership.id)
    return logs


def _drop_memberships_from_staff_api(membership_ids, chunk_size=10):
    """Удаляем groupmembership'ы из staff-api, создавая записи на удаление в emission.MasterLog"""
    from staff.emission.django.emission_master.models import MasterLog
    from staff.lib.utils.list import paginate

    data_stub = {
        'fields': {
            'group': 9250474448,  # несуществующие id
            'joined_at': '2018-02-22T22:22:22',
            'staff': 4522384830,  # несуществующие id
        },
        'model': 'django_intranet_stuff.groupmembership',
        'pk': 0
    }

    logs = []
    for ids_chunk in paginate(membership_ids, chunk_size):
        data_list = []
        for pk in ids_chunk:
            data_chunk = data_stub.copy()
            data_chunk['pk'] = pk
            data_list.append(data_chunk)

        masterlog_object = MasterLog(
            data=json.dumps(data_list),
            action='delete'
        )
        masterlog_object.save()
        log = {'masterlog_id': masterlog_object.id, 'memberships': [p['pk'] for p in data_list]}
        logger.info(
            'Created MasterLog object %s for deleting memberships %s',
            log['masterlog_id'],
            log['memberships']
        )
        logs.append(log)
    return logs
