from collections import defaultdict
from functools import partial
from itertools import chain, groupby
from typing import Any, Dict, List, Union, Tuple, Iterable

from staff.lib.models.mptt import filter_by_heirarchy, is_ancestor
from staff.lib.utils.qs_values import extract_related

from staff.departments.models import Department, DepartmentRoles, DepartmentStaff
from staff.person.models.person import Staff


Role = Dict[str, List[Dict[str, Union[int, str]]]]  # {role: [{'id': 123, 'login': 'abc'}, ]}
DepDataType = Dict[str, Any]
DepListType = Union[List[Department], List[DepDataType]]
FieldsType = Union[Iterable[str], None]


def all_persons_with_roles(roles: List[str]) -> Dict[int, Role]:
    """{dep_id: {role:[{'id': person_id, 'login': person_login}, ], }}"""
    roles_data = (
        DepartmentStaff.objects
        .filter(role_id__in=roles)
        .values_list('staff_id', 'staff__login', 'department_id', 'role')
        .order_by('department', 'role')
    )

    dep_role_staff = defaultdict(lambda: defaultdict(list))

    for staff_id, login, department_id, role in roles_data:
        dep_role_staff[department_id][role].append(
            {
                'id': staff_id,
                'login': login,
            }
        )
    return dep_role_staff


def extract_chiefs(roles_structure):
    """{dep_id: {'id': id, 'login': login}}"""
    return {
        dep_id: roles[DepartmentRoles.CHIEF.value][0]
        for dep_id, roles in roles_structure.items()
        if DepartmentRoles.CHIEF.value in roles
    }


def extract_role(roles_structure, role):
    """{dep_id: [{'id': id, 'login': login}]}"""
    return {
        dep_id: roles[role]
        for dep_id, roles in roles_structure.items()
        if role in roles
    }


def get_all_chiefs():
    roles = all_persons_with_roles([DepartmentRoles.CHIEF.value])
    return extract_chiefs(roles)


def get_chiefs_chains():
    result = {}
    dep_qs = (
        Department.objects
        .order_by('level')
        .values('id', 'parent_id')
    )

    chief_by_dep_id = get_all_chiefs()

    for dep in dep_qs:
        dep_id = dep['id']

        parent_chain = result.get(dep['parent_id'], [])
        chain = parent_chain[:]
        chief = chief_by_dep_id.get(dep_id)

        if chief and chief not in chain:
            chain.append(chief)

        result[dep_id] = chain

    return result


def get_chiefs_by_persons(person_list):
    result = []
    chiefs_chains = get_chiefs_chains()

    for _id, login, dep_id in person_list:
        result.append({
            'person': {'id': _id, 'login': login},
            'chiefs': [
                chief
                for chief in reversed(chiefs_chains[dep_id])
                if chief['id'] != _id
            ]
        })

    return result


def _get_roles_qs_fields(fields: FieldsType):
    fields = {'staff__' + f for f in fields} if fields else set()
    fields |= {'staff__id', 'role_id', 'department__id', 'department__tree_id', 'department__lft', 'department__rght'}
    return fields


def _get_roles_qs(roles: Iterable[str], fields: FieldsType):
    fields = _get_roles_qs_fields(fields=fields)

    roles_qs = (
        DepartmentStaff.objects
        .filter(role_id__in=roles)
        .values(*fields)
        .order_by('department__tree_id', '-department__lft', '-role__position', '-id')
    )
    return roles_qs


def get_roles_by_departments_qs(department_list: DepListType, roles: Iterable[str], fields: FieldsType = None):
    """
    :param department_list: список объектов подразделений или словари с mptt ключами
    :param fields: Список строк с именами полей, которые надо добавить к служебным в запрос.
    :return: QuerySet DepartmentStaff c иерархической фильтрацией по списку подразделений,
             фильтром по роли и правильной сортировкой.
    """

    roles_qs = _get_roles_qs(roles=roles, fields=fields)
    roles_qs = filter_by_heirarchy(
        query_set=roles_qs,
        mptt_objects=department_list,
        by_children=False,
        filter_prefix='department__',
        include_self=True,
    )

    return roles_qs


def get_grouped_hrbp_by_departments(department_list: DepListType, fields: FieldsType = None):
    """
    :param department_list: список объектов подразделений или словари с mptt ключами
    :param fields: Список строк с именами полей, которые надо добавить к служебным в запрос.
    :return: словарь где ключи id подразделений, а в значениях списоки словарей с данными HR-партнеров
    """
    roles_qs = get_roles_by_departments_qs(
        department_list=department_list,
        roles=[DepartmentRoles.HR_PARTNER.value],
        fields=fields
    )

    group_key = partial(extract_related, related_name='department', pop=False)

    def get_dep_id():
        return dep['id'] if isinstance(dep, dict) else dep.id

    result = defaultdict(list)
    for dep in department_list:
        for department, roles in groupby(roles_qs, key=group_key):
            if is_ancestor(department, dep):
                for role in roles:
                    result[get_dep_id()].append(extract_related(role, 'staff', pop=False))
                break

    return result


def get_hrbp_by_departments(department_list: DepListType, fields: FieldsType = None):
    """
    :param department_list: список объектов подразделений или словари с mptt ключами
    :param fields: Список строк с именами полей, которые надо добавить к служебным в запрос.
    :return: генератор словарей с данными Сотрудников, которые являются HR-партнерами для списка подразделений
    """
    return chain.from_iterable(
        get_grouped_hrbp_by_departments(department_list=department_list, fields=fields).values()
    )


def get_grouped_hrbp_by_persons(person_list, fields: FieldsType = None):
    if any(isinstance(it, dict) for it in person_list):
        department_list = list({p['department']['id']: p['department'] for p in person_list}.values())
    else:
        department_list = [it.department for it in person_list]
    partners_by_deps = get_grouped_hrbp_by_departments(department_list=department_list, fields=fields)
    result = {}
    for person in person_list:
        if isinstance(person, dict):
            person_id, dep_id = person['id'], person['department']['id']
        else:
            person_id, dep_id = person.id, person.department.id
        if dep_id in partners_by_deps:
            result[person_id] = partners_by_deps[dep_id]
    return result


class DepartmentData(dict):
    def __eq__(self, other):
        return self['id'] == other['id']

    def __hash__(self):
        return hash(self['id'])


def get_grouped_roles_chains_by_departments(
    department_list: DepListType,
    roles: Iterable[str],
    fields: FieldsType = None
) -> Dict[int, List[List[Dict]]]:
    """
    :param department_list: список объектов подразделений или словари с mptt ключами
    :param fields: Список строк с именами полей, которые надо добавить к служебным в запрос.
    :param roles: Список ролей которые учитывать как chief
    :return: словарь где ключи id подразделений,
        а значения - список списков ролей, где каждый список ролей - это роли какого-то из родителей начиная с себя
    """
    roles_qs = get_roles_by_departments_qs(
        department_list=department_list,
        roles=roles,
        fields=fields
    )

    roles_and_departments = (
        (DepartmentData(extract_related(role, related_name='department', pop=False)), role)
        for role in roles_qs
    )

    roles_and_departments = {
        dep_data: [item[1] for item in items]
        for dep_data, items in groupby(roles_and_departments, key=lambda item: item[0])
    }

    result = {}
    for dep in department_list:
        dep_id = dep['id'] if isinstance(dep, dict) else dep.id

        result[dep_id] = [
            roles
            for dep_data, roles in roles_and_departments.items()
            if is_ancestor(dep_data, dep)
        ]
    return result


def get_grouped_chiefs_by_departments(
    department_list: DepListType,
    fields: FieldsType = None,
    roles: Iterable[str] or None = None
) -> Dict[int, Dict]:
    """
    :param department_list: список объектов подразделений или словари с mptt ключами
    :param fields: Список строк с именами полей, которые надо добавить к служебным в запрос.
    :param roles: Список ролей которые учитывать как chief
    :return: словарь где ключи id подразделений, а в значениях данные руководителя
    """
    roles = roles if roles else [DepartmentRoles.CHIEF.value]

    roles_chains = get_grouped_roles_chains_by_departments(
        department_list=department_list,
        roles=roles,
        fields=fields,
    )

    return {dep_id: roles[0][0] if roles else {} for dep_id, roles in roles_chains.items()}


def get_chiefs_by_departments(department_list: DepListType, fields: FieldsType = None):
    """
    :param department_list: список объектов подразделений или словари с mptt ключами
    :param fields: Список строк с именами полей, которые надо добавить к служебным в запрос.
    :return: генератор словарей с данными Сотрудников, которые являются CHIEF для списка подразделений
    """
    return chain.from_iterable(
        get_grouped_chiefs_by_departments(department_list=department_list, fields=fields).values()
    )


def direct_chief_for_department(department, fields: FieldsType = None):
    """
    :rtype: dict
    :param department: объект подразделения или словарь с mptt ключами
    :param fields: Список строк с именами полей, которые надо добавить к служебным в запрос.
    :return: словарь с данными CHIEF для данного подразделения или None если такого нет
    """
    result = get_roles_by_departments_qs([department], [DepartmentRoles.CHIEF.value], fields=fields).first()
    return extract_related(result, 'staff') if result else None


def chiefs_chain_for_person(person: Staff, fields: FieldsType = None, roles=None):
    """
    :rtype: dict
    :param person: обьект Staff
    :param roles: Список строк с кодами ролей, которые нужно учитывать при построении списка руководителей.
    :param fields: Список строк с именами полей, которые надо добавить к служебным в запрос.
    :return: список со словарями с данными CHIEF от данного человека и вверх по иерархии, исключая его самого
    """
    roles = roles or [DepartmentRoles.CHIEF.value]
    result = (
        get_roles_by_departments_qs(
            department_list=[person.department],
            roles=roles,
            fields=fields,
        )
        .exclude(staff=person)
    )
    return [extract_related(department_role, 'staff') for department_role in result]


def direct_hr_partners_for_department(department, fields=None):
    """
    :rtype: dict
    :param department: объект подразделения или словарь с mptt ключами
    :param fields: Список строк с именами полей, которые надо добавить к служебным в запрос.
    :return: список со словарями с данными HR_PARTNER для данного подразделения или None если такого нет
    """
    roles_qs = get_roles_by_departments_qs([department], [DepartmentRoles.HR_PARTNER.value], fields=fields)
    group_key = partial(extract_related, related_name='department', pop=False)

    for department, roles in groupby(roles_qs, key=group_key):
        return [extract_related(role, 'staff') for role in roles]

    return None


def has_roles_for_department(department: Union[Department, Dict], staff: Staff, roles: Union[List, Tuple]) -> bool:
    """
    :param department: объект подразделения или словарь с mptt ключами
    :return: True если staff имеет одну из ролей в roles в цепочке до верхушки дерева, иначе False
    """

    return get_roles_by_departments_qs([department], roles).filter(staff=staff).exists()


def has_role(person: Staff, roles: Union[List, Tuple]) -> bool:
    return DepartmentStaff.objects.filter(staff=person, role_id__in=roles).exists()
