import itertools
import logging
from collections import defaultdict
from typing import Dict, Any, Optional, Collection, List

from django.utils.encoding import force_text

from idm.api.frontend.serializers.base import Serializer, ListSerializer
from idm.core import mutation
from idm.core.constants.rolefield import FIELD_TYPE
from idm.core.constants.system import SYSTEM_STATE
from idm.core.models import System, Role, RoleNode, RoleField
from idm.core.utils import (
    humanize_role, get_localized_value, get_localized_node_name, get_localized_node_description,
    humanize_role_state, get_system_state, get_role_review_at
)
from idm.users.utils import get_user_fullname, get_localized_group_name, get_group_url
from idm.utils.i18n import get_lang_key

log = logging.getLogger(__name__)


def enrich_role_fields(role_data: Dict[str, Any], role_fields: List[RoleField] = None):
    """
    Обогащает сериализованный объект роли данными полей роли.
    Изменяет поле system_specific если оно не пусто, иначе если изменяет fields_data.
    Если оба поля пусты, оставляет объект без изменений.

    Args:
        role_data: Сериализованный объект роли
        role_fields: Список объектов RoleNode связанных с ролью. Если пустой - поля загружаются из базы

    """
    if not (fields_data := (role_data.get('system_specific') or role_data.get('fields_data'))):
        return
    if role_fields is None:
        role_node = RoleNode.objects.get(roles__id=role_data['id'])
        role_fields = RoleField.objects.filter(
            is_active=True,
            type=FIELD_TYPE.CHOICE,
            node__in=RoleNode.objects.filter(rolenodeclosure_children__child=role_node)
        )

    fields_extra = {}
    for role_field in role_fields:
        if field_names := role_field.get_value_name_map():
            fields_extra[role_field.slug] = field_names

    for field, value in fields_data.items():
        if field not in fields_extra or value not in fields_extra[field]:
            continue
        fields_data[field] = fields_extra[field][value]


class SlugOnlyGroupSerializer(Serializer):
    """Для генерации ссылки на скоуп нужен слаг самого сервиса"""
    db_fields = ['slug']


class GroupSerializer(Serializer):
    db_fields = [
        'created_at', 'slug', 'external_id', 'state', 'name', 'name_en', 'type', 'updated_at', 'level',
    ]

    datetime_fields = [
        'created_at', 'updated_at',
    ]

    complex_fields = {
        'parent': SlugOnlyGroupSerializer
    }

    fields_to_delete = [
        'name_en', 'level', 'parent',
    ]

    @classmethod
    def process_obj_fields(cls, obj, **kwargs):
        obj['id'] = obj.pop('external_id')
        obj['url'] = get_group_url(obj)
        obj['name'] = get_localized_group_name(obj, lang=kwargs.get('lang'))


class SystemSerializer(Serializer):
    db_fields = [
        'id', 'description', 'endpoint_timeout', 'endpoint_long_timeout', 'group_policy', 'is_active',
        'is_broken', 'name', 'name_en', 'is_sox', 'name', 'slug', 'use_mini_form', 'has_review',
        'roles_review_days',
    ]

    fields_to_delete = ['name_en', ]

    @classmethod
    def process_obj_fields(cls, obj, **kwargs):
        obj['name'] = get_localized_value(obj, 'name', lang=kwargs.get('lang'))
        obj['state'] = force_text(dict(SYSTEM_STATE.CHOICES)[get_system_state(obj)])


class NodeSerializer(Serializer):
    db_fields = [
        'id', 'state', 'slug', 'data', 'is_public', 'is_auto_updated', 'is_key', 'unique_id', 'slug_path',
        'value_path', 'name', 'name_en', 'description', 'description_en', 'fullname', 'nodeset__set_id',
    ]

    fields_to_delete = [
        'nodeset', 'description_en', 'fullname', 'name_en',
    ]

    @classmethod
    def process_obj_fields(cls, obj, **kwargs):
        lang = kwargs.get('lang')
        obj['name'] = get_localized_node_name(obj, lang=lang)
        obj['description'] = get_localized_node_description(obj, lang=lang)
        obj['set'] = obj['nodeset']['set_id'] if obj['nodeset'] else None


class UserSerializer(Serializer):
    db_fields = [
        'username', 'email', 'sex', 'is_active', 'position', 'date_joined', 'fired_at', 'type', 'first_name',
        'first_name_en', 'last_name', 'last_name_en',
    ]

    datetime_fields = [
        'date_joined', 'fired_at',
    ]

    fields_to_delete = ['first_name', 'first_name_en', 'last_name', 'last_name_en']

    @classmethod
    def process_obj_fields(cls, obj, **kwargs):
        obj['full_name'] = get_user_fullname(obj, lang=kwargs.get('lang'))
        obj['username'] = str(obj['username'])


class ParentlessRoleSerializer(Serializer):
    db_fields = [
        'fields_data', 'system_specific', 'added', 'updated', 'expire_at', 'granted_at', 'review_at',
        'id', 'is_active', 'is_public', 'state', 'ttl_date', 'ttl_days', 'review_date', 'review_days',
        'with_inheritance', 'with_robots', 'with_external', 'without_hold', 'parent_id', 'system',
    ]

    datetime_fields = [
        'added', 'updated', 'expire_at', 'granted_at', 'review_at', 'ttl_date', 'review_date',
    ]

    complex_fields = {
        'group': GroupSerializer,
        'node': NodeSerializer,
        'user': UserSerializer,
    }

    @classmethod
    def remove_redundant_parent(cls, obj):
        obj['parent'] = obj.get('parent_id')

    @classmethod
    def process_obj_fields(cls, obj, **kwargs):
        lang = kwargs.get('lang')
        obj['human'] = obj['node']['human'] = humanize_role(obj, lang=lang)
        obj['human_short'] = obj['node']['human_short'] = humanize_role(obj, format='short', lang=lang)
        obj['human_state'] = force_text(humanize_role_state(obj))
        obj['data'] = obj['node']['data']
        obj['review_at'] = get_role_review_at(obj)
        cls.remove_redundant_parent(obj)

    @classmethod
    def postprocess(cls, role_data: Dict[str, Any], **kwargs):
        if kwargs.get('resource_type') == 'frontend':
            super().postprocess(role_data, **kwargs)
            if mutation.RoleFieldMetrikaCounterMutation.is_applicable(role_data):
                mutation.RoleFieldMetrikaCounterMutation.mutate(role_data['fields_data'])
            if mutation.RoleFieldAppMetricaMutation.is_applicable(role_data):
                mutation.RoleFieldAppMetricaMutation.mutate(role_data['fields_data'])
        if kwargs.get('enrich_fields', True):
            enrich_role_fields(role_data)


class RoleSerializer(ParentlessRoleSerializer):
    db_fields = [
        'fields_data', 'system_specific', 'added', 'updated', 'expire_at', 'granted_at', 'review_at',
        'id', 'is_active', 'is_public', 'state', 'ttl_date', 'ttl_days', 'review_date', 'review_days',
        'with_inheritance', 'with_robots', 'with_external', 'without_hold', 'system',
    ]

    complex_fields = {
        'group': GroupSerializer,
        'node': NodeSerializer,
        'parent': ParentlessRoleSerializer,
        'user': UserSerializer,
    }

    @classmethod
    def remove_redundant_parent(cls, obj):
        pass


class RolesListSerializer(ListSerializer):
    obj_serializer = RoleSerializer

    @classmethod
    def fill_systems(cls, roles_data: Collection[Dict], lang: str):
        systems_pks = set()
        for role_data in roles_data:
            systems_pks.add(role_data['system'])
            if role_data.get('parent'):
                systems_pks.add(role_data['parent']['system'])

        systems = {}
        for system_data in (
            System.objects
                .filter(pk__in=systems_pks)
                .nested_values(*SystemSerializer.get_values_list_args())
        ):
            SystemSerializer.process(system_data, lang=lang)
            systems[system_data['id']] = system_data

        for role_data in roles_data:
            role_data['system'] = role_data['node']['system'] = systems[role_data['system']]
            if role_data.get('parent'):
                role_data['parent']['system'] = \
                    role_data['parent']['node']['system'] = systems[role_data['parent']['system']]

    @classmethod
    def enrich_fields(cls, roles_data: Collection[Dict]):
        """
        Для перечня ролей подгружается список данных о полях связанных с узлом роли, и с корневым узлом системы.
        Далее для каждой роли эти поля проставляются
        Args:
            roles_data: Список словарей - сериализованных ролей
        """
        role_fields = defaultdict(list)
        node_ids = set()
        roles_with_fields = []

        for role_data in roles_data:
            if role_data.get('system_specific') or role_data.get('fields_data'):
                roles_with_fields.append(role_data)
                node_ids.add(role_data['node']['id'])

        if not roles_with_fields:
            return

        # для каждого узла нужно собрать всю цепочку предков
        node_ancestor_ids = defaultdict(set)
        for node_id, ancestor_id in (
            RoleNode.objects
                .filter(rolenodeclosure_children__child__in=node_ids)
                .values_list('id', 'rolenodeclosure_children__child_id')
        ):
            if node_id != ancestor_id:
                node_ancestor_ids[ancestor_id].add(node_id)

        # все поля принадлежащие узлам роли и их предкам
        for role_field in RoleField.objects.filter(
            is_active=True,
            type=FIELD_TYPE.CHOICE,
            node_id__in=node_ids | set(itertools.chain(*node_ancestor_ids.values())),
        ):
            role_fields[role_field.node_id].append(role_field)

        for role_data in roles_with_fields:
            node_fields = role_fields.get(role_data['node']['id'], [])
            for ancestor_id in node_ancestor_ids[role_data['node']['id']]:
                node_fields.extend(role_fields.get(ancestor_id, []))
            enrich_role_fields(role_data, node_fields)

    @classmethod
    def process_list(
        cls,
        roles_data: Collection[Dict[str, Any]],
        lang: Optional[str] = None,
        resource_type: Optional[str] = None,
        **kwargs,
    ):
        lang = lang or get_lang_key()
        cls.fill_systems(roles_data, lang=lang)
        super().process_list(roles_data, lang=lang, resource_type=resource_type, enrich_fields=False)
        cls.enrich_fields(roles_data)
