# coding: utf-8
from __future__ import unicode_literals

import logging

import attr
import yenv

from cab.sources import review
from cab.utils import multifetch

from . import const, fetchers, processors


log = logging.getLogger(__name__)

_NOT_SET = object()


def _is_enough_data_fetched(person_identifier, fetched_data):
    if person_identifier not in fetched_data:
        return False
    data_for_person = fetched_data[person_identifier]
    if isinstance(data_for_person, basestring):
        return data_for_person != review.NO_ACCESS
    elif isinstance(data_for_person, (list, tuple, set)):
        return review.NO_ACCESS not in data_for_person
    elif isinstance(data_for_person, dict):
        return review.NO_ACCESS not in data_for_person.values()
    else:
        return True


def add_data(auth, persons, fields):
    fetchers_with_fields = _route_fields_by_fetchers(fields)
    args_dict = {
        fetcher_id: (auth, persons, fields)
        for fetcher_id, fields in fetchers_with_fields.items()
    }
    raw_data = multifetch.collect_data(
        env_prefix='persons',
        func=fetch,
        args_dict=args_dict,
    )
    for person in persons:
        for field in fields:
            set_field(person, field, raw_data)


@multifetch.fetcher
def fetch(args):
    fetcher_id, func_args = args[0], args[1:]
    return getattr(fetchers, 'fetch_' + fetcher_id)(*func_args)


def set_field(person, field_name, raw_data):
    if field_name not in FIELDS:
        log.warning('Unknown field %s in set_field', field_name)
    fetch_ids = _get_fetch_ids_for_field(field_name)

    fetch_results = {
        fetch_id: raw_data[fetch_id]
        for fetch_id in fetch_ids
    }

    errors_exist = any(
        fetcher_result.is_error
        for fetcher_result in fetch_results.values()
    )
    if errors_exist:
        person[field_name] = const.FIELD_FETCH_ERROR
        return

    fetch_person_identifiers = {
        fetch_id: FETCHERS[fetch_id].get('identifier', 'id')
        for fetch_id in fetch_ids
    }

    on_missing_value = FIELDS[field_name].get('on_missing', const.FIELD_NO_DATA)
    on_none_value = FIELDS[field_name].get('on_none', const.NOT_SET)

    is_enough_data = all(
        _is_enough_data_fetched(
            person_identifier=person[fetch_person_identifiers[fetch_id]],
            fetched_data=fetch_result.value,
        )
        for fetch_id, fetch_result in fetch_results.items()
    )
    if not is_enough_data:
        person[field_name] = on_missing_value
        return

    if on_none_value != const.NOT_SET:
        is_none_somewhere = any(
            fetch_result.value[person[fetch_person_identifiers[fetch_id]]] is None
            for fetch_id, fetch_result in fetch_results.items()
        )
        if is_none_somewhere:
            person[field_name] = on_none_value

    try:
        data = {}
        for fetch_id, fetch_result in fetch_results.items():
            person_id_for_fetch = person[fetch_person_identifiers[fetch_id]]
            data[fetch_id] = fetch_result.value[person_id_for_fetch]

        if len(data) == 1:
            data = data.values()[0]
        person[field_name] = process_field(
            field_name=field_name,
            data=data,
        )
    except Exception:
        log.exception('Processor for %s failed', field_name)
        person[field_name] = const.FIELD_PROCESS_ERROR
        if yenv.type == 'development':
            raise


def process_field(field_name, data):
    field_processor_name = 'process_' + field_name
    field_processor = getattr(processors, field_processor_name, None)
    if field_processor:
        return field_processor(data)

    fetch_ids = _get_fetch_ids_for_field(field_name)
    if len(fetch_ids) != 1:
        raise RuntimeError(
            "Field processor `%s` not found",
            field_processor_name,
        )
    else:
        fetch_id = fetch_ids[0]
    source_processor_name = 'process_' + fetch_id + '_field'
    source_processor = getattr(processors, source_processor_name, None)

    if source_processor:
        return source_processor(data, field_name)

    raise RuntimeError(
        "No %s or %s for processing %s",
        field_processor_name,
        source_processor_name,
        field_name,
    )


@attr.s
class RawData(object):
    data = attr.ib(default=attr.Factory(dict))
    errors = attr.ib(default=attr.Factory(dict))


# поля относятся к фетчерам как многие-ко-многим (из одного типа данных
# можно сформировать несколько полей, а некоторым полям нужно несколько
# типов данных.
FETCHERS = {
    'staff_whistlah': {
        'identifier': 'login',
    },
    'staff_personal': {
        'identifier': 'login',
    },
    'staff_api_person': {
        'identifier': 'login',
    },
    'subordinate_departments': {
        'identifier': 'login',
        'bulk': False,
    },
    'subordinate_persons': {
        'identifier': 'login',
        'bulk': False,
    },
    'gap_vacations': {
        'identifier': 'login',
    },
    'goals': {
        'identifier': 'id',
    },
    'review': {
        'identifier': 'login',
    },
    'review_income_list': {
        'identifier': 'login',
    },
    'review_assignments_list': {
        'identifier': 'login',
    },
    'finance': {
        'identifier': 'login',
    },
    'gap': {
        'identifier': 'login',
    }
}

# поля отсортированы по (fetch, field_name)
# в fetch может быть строка (если для поля нужны данные одного типа) или
# список (для поля нужны данные нескольких типов,
# например для совокупного дохода нужны зарплаты, бонусы, опционы)
FIELDS = {
    'gap': {
        'fetch': 'gap',
    },
    'last_vacation': {
        'fetch': 'gap_vacations',
    },
    'vacation_days_planned': {
        'fetch': 'gap_vacations',
    },
    'vacation_days_used': {
        'fetch': 'gap_vacations',
    },
    'goals_count': {
        'fetch': 'goals',
    },
    'key_goals_involvement': {
        'fetch': 'goals',
    },
    'last_bonus_payment': {
        'fetch': 'finance',
        'on_missing': const.FIELD_ACCESS_DENIED,
    },
    'loans': {
        'fetch': 'finance',
        'on_missing': const.FIELD_ACCESS_DENIED,
    },
    'fte': {
        'fetch': 'review_assignments_list',
        'on_missing': const.FIELD_ACCESS_DENIED,
    },
    'grade': {
        'fetch': 'finance',
        'on_missing': const.FIELD_ACCESS_DENIED,
        'on_none': const.FIELD_NO_DATA,
    },
    'last_salary_raise': {
        'fetch': 'finance',
        'on_missing': const.FIELD_ACCESS_DENIED,
    },
    'social_package': {
        'fetch': 'finance',
        'on_missing': const.FIELD_ACCESS_DENIED,
        'on_none': const.FIELD_NO_DATA,
    },
    'bonus_options': {
        'fetch': 'review',
        'on_missing': const.FIELD_ACCESS_DENIED,
    },
    'marks': {
        'fetch': 'review',
        'on_missing': const.FIELD_ACCESS_DENIED,
    },
    'reviews': {
        'fetch': 'review',
        'on_missing': const.FIELD_ACCESS_DENIED,
    },
    'salary': {
        'fetch': 'review_income_list',
        'on_missing': const.FIELD_ACCESS_DENIED,
    },
    'year_bonus_payment': {
        'fetch': 'review_income_list',
        'on_missing': const.FIELD_ACCESS_DENIED,
    },
    'salary_forecast': {
        'fetch': 'review_income_list',
        'on_missing': const.FIELD_ACCESS_DENIED,
    },
    'bonus_forecast': {
        'fetch': 'review_income_list',
        'on_missing': const.FIELD_ACCESS_DENIED,
    },
    'options_vesting_forecast_cost': {
        'fetch': 'review_income_list',
        'on_missing': const.FIELD_ACCESS_DENIED,
    },
    'incomes': {
        'fetch': 'review_income_list',
        'on_missing': const.FIELD_ACCESS_DENIED,
    },
    'income_forecast': {
        'fetch': 'review_income_list',
        'on_missing': const.FIELD_ACCESS_DENIED,
    },
    'signup': {
        'fetch': 'review_income_list',
        'on_missing': const.FIELD_ACCESS_DENIED,
    },
    'piecerate': {
        'fetch': 'review_income_list',
        'on_missing': const.FIELD_ACCESS_DENIED,
    },
    # _last_year поля не нужны после STAFF-8385
    'income_last_year': {
        'fetch': 'finance',
        'on_missing': const.FIELD_ACCESS_DENIED,
        'on_none': const.FIELD_NO_DATA,
    },
    'options_monetary_last_year': {
        'fetch': 'finance',
        'on_missing': const.FIELD_ACCESS_DENIED,
        'on_none': const.FIELD_NO_DATA,
    },
    'bonus_last_year': {
        'fetch': 'finance',
        'on_missing': const.FIELD_ACCESS_DENIED,
        'on_none': const.FIELD_NO_DATA,
    },
    'salary_last_year': {
        'fetch': 'finance',
        'on_missing': const.FIELD_ACCESS_DENIED,
        'on_none': const.FIELD_NO_DATA,
    },
    'options_monetary_forecast': {
        'fetch': 'finance',
        'on_missing': const.FIELD_ACCESS_DENIED,
        'on_none': const.FIELD_NO_DATA,
    },
    'birthday': {
        'fetch': 'staff_api_person',
    },
    'city': {
        'fetch': 'staff_api_person',
    },
    'death_date': {
        'fetch': 'staff_api_person',
    },
    'department': {
        'fetch': 'staff_api_person',
    },
    'employment': {
        'fetch': 'staff_api_person',
    },
    'first_name': {
        'fetch': 'staff_api_person',
    },
    'gender': {
        'fetch': 'staff_api_person',
    },
    'id': {
        'fetch': 'staff_api_person',
    },
    'is_dismissed': {
        'fetch': 'staff_api_person',
    },
    'join_at': {
        'fetch': 'staff_api_person',
    },
    'last_name': {
        'fetch': 'staff_api_person',
    },
    'office': {
        'fetch': 'staff_api_person',
    },
    'organization': {
        'fetch': 'staff_api_person',
    },
    'position': {
        'fetch': 'staff_api_person',
    },
    'quit_at': {
        'fetch': 'staff_api_person',
    },
    'superior': {
        'fetch': 'staff_api_person',
    },
    'work_email': {
        'fetch': 'staff_api_person',
    },
    'work_phone': {
        'fetch': 'staff_api_person',
    },
    'food': {
        'fetch': 'staff_personal',
        'on_missing': const.FIELD_NO_DATA,
    },
    'password': {
        'fetch': 'staff_personal',
        'on_missing': const.FIELD_NO_DATA,
    },
    'vacation_days_accumulated': {
        'fetch': 'staff_personal',
        'on_missing': const.FIELD_NO_DATA,
    },
    'last_activity': {
        'fetch': 'staff_whistlah',
    },
    'subordinate_departments': {
        'fetch': 'subordinate_departments',
    },
    'direct_subordinates': {
        'fetch': 'subordinate_persons',
    },
    'direct_subordinates_count': {
        'fetch': 'subordinate_persons',
    },
    'total_subordinates': {
        'fetch': 'subordinate_persons',
    },
    'total_subordinates_count': {
        'fetch': 'subordinate_persons',
    },
}


def _get_fetch_ids_for_field(field):
    fetch_id = FIELDS[field]['fetch']
    if isinstance(fetch_id, basestring):
        return [fetch_id]
    return fetch_id


def _route_fields_by_fetchers(fields):
    fields = set(fields)
    routed = {}

    for field in fields:
        fetch_ids = _get_fetch_ids_for_field(field)
        for fetch_id in fetch_ids:
            fetcher_fields = routed.setdefault(fetch_id, set())
            fetcher_fields.add(field)
    return routed


def _get_bulk_fields():
    fields = []
    for field in ALL_FIELDS:
        fetch_ids = _get_fetch_ids_for_field(field)
        fetch_metas = [FETCHERS[fetch_id] for fetch_id in fetch_ids]
        if all(meta.get('bulk', True) for meta in fetch_metas):
            fields.append(field)
    return fields


ALL_FIELDS = tuple(FIELDS)
ALL_BULK_FIELDS = tuple(_get_bulk_fields())
