from collections import defaultdict
import logging
from typing import Any
import uuid

from intranet.trip.src.api.auth import get_tvm_service_ticket
from intranet.trip.src.config import settings
from intranet.trip.src.enums import Provider
from intranet.trip.src.lib.staffapi.gateway import StaffApiGateway
from intranet.trip.src.models import Person
from intranet.trip.src.unit_of_work import UnitOfWork

logger = logging.getLogger(__name__)


ChiefRecord = tuple[int, int, bool]  # (owner, dependant, is_direct)


class StaffApiProfileSync:
    """Класс, в котором описана логика синхронизации Профилей Person и сотрудников Staff"""
    def __init__(self, uow: UnitOfWork, staffapi_service_ticket):
        self.uow = uow
        self.staffapi_gateway = StaffApiGateway(service_ticket=staffapi_service_ticket)
        self.company_ids: set[int] = None
        self.person_ids_with_person_trip: set[int] = None

    @classmethod
    async def init(cls, uow: UnitOfWork):
        return cls(
            uow=uow,
            staffapi_service_ticket=await get_tvm_service_ticket('staff-api'),
        )

    async def sync(self) -> list[ChiefRecord]:
        self.company_ids = await self._get_all_companies()
        self.person_ids_with_person_trip = await self.uow.persons.get_all_ids_with_person_trip()

        counters = defaultdict(int)
        chief_records: list[ChiefRecord] = []
        async with self.uow:
            async for staffapi_persons_chunk in self.staffapi_gateway.get_persons_by_chunks():
                chief_records.extend(StaffApiRolesSync.process_chunk(staffapi_persons_chunk))

                added, updated, dismissed = (
                    await self.process_chunk(staffapi_persons_chunk)
                )
                counters['added'] += added
                counters['updated'] += updated
                counters['dismissed'] += dismissed

        logger.info(
            'sync_staffapi_profiles finished. Added: %s, Updated: %s, Dismissed: %s',
            counters['added'],
            counters['updated'],
            counters['dismissed'],
        )

        return chief_records

    async def _get_all_companies(self) -> set[int]:
        """Возвращает множество всех айдишников компаний кроме "вне организаций" """
        company_ids = await self.uow.companies.get_all_company_ids()
        company_ids = set(company_ids) - {self.uow.companies.out_of_company_id}
        return company_ids

    def _flat_fields(self, person_record: dict[str, Any]) -> dict[str, Any]:
        """
        Из записи person формата staff-api
        делаем плоский список полей, которые нужны нашей базе
        """
        name = person_record['name']
        phone = self.staffapi_gateway.extract_phone_number(person_record['phones'])
        company_id = person_record['official']['organization']['id']
        if company_id not in self.company_ids:
            company_id = self.uow.companies.out_of_company_id
        is_active = (
            not person_record['official']['is_dismissed']
            and person_record['official']['affiliation'] == 'yandex'
        )

        return {
            'person_id': person_record['id'],
            'uid': person_record['uid'],
            'external_uid': uuid.uuid4(),
            'login': person_record['login'],

            'first_name': name['first']['ru'],
            'last_name': name['last']['ru'],
            'middle_name': name['middle'],
            'first_name_en': name['first']['en'],
            'last_name_en': name['last']['en'],
            'middle_name_en': name['middle'],

            'gender': person_record['personal']['gender'],
            'company_id': company_id,
            'phone_number': phone,
            'is_dismissed': person_record['official']['is_dismissed'],
            'is_active': is_active,
            'email': person_record['work_email'],
        }

    @staticmethod
    def diff_fields(person: Person, person_data: dict) -> dict[str, Any]:
        diff = {}
        check_fields = [
            'login', 'uid',
            'first_name', 'last_name', 'middle_name',
            'first_name_en', 'last_name_en', 'middle_name_en',
            'gender', 'company_id', 'phone_number', 'is_dismissed', 'email', 'is_active',
        ]
        for field_name in check_fields:
            if getattr(person, field_name) != person_data[field_name]:
                diff[field_name] = person_data[field_name]
        return diff

    async def process_chunk(
            self,
            staffapi_persons_chunk: list[dict],
    ) -> tuple[int, int, int]:
        added = updated = dismissed = 0
        first_id = staffapi_persons_chunk[0]['id']
        last_id = staffapi_persons_chunk[-1]['id']
        staffapi_persons = {p['id']: p for p in staffapi_persons_chunk}
        db_persons = {
            p.person_id: p
            for p in await self.uow.persons.get_for_staffapi_sync(first_id, last_id)
        }

        for person_id, person_data in staffapi_persons.items():
            api_person = self._flat_fields(person_data)
            db_person = db_persons.pop(person_id, None)

            if db_person:
                diff_fields = self.diff_fields(db_person, api_person)
                if diff_fields:
                    logger.info('need update %s (%s): %s', person_id, db_person.login, diff_fields)
                    await self.uow.persons.update(person_id, **diff_fields)
                    if (
                        db_person.provider_profile_id
                        and set(diff_fields.keys()) - {'is_dismissed', 'email'}
                        and person_id in self.person_ids_with_person_trip
                    ):
                        self.uow.add_job(
                            job_name='sync_profiles_to_ihub_task',
                            person_ids=[person_id],
                            unique=False,
                        )
                    updated += 1
            else:
                api_person.update({
                    'first_name_ac': api_person['first_name'],
                    'last_name_ac': api_person['last_name'],
                    'middle_name_ac': api_person['middle_name'],
                })
                await self.uow.persons.create(**api_person)
                added += 1

        for person_id, person in db_persons.items():
            if not person.is_dismissed:
                await self.uow.persons.update(person_id, is_dismissed=True)
                dismissed += 1

        return (added, updated, dismissed)


class StaffApiRolesSync:

    def __init__(self, uow: UnitOfWork, staffapi_service_ticket):
        self.uow = uow
        self.staffapi_gateway = StaffApiGateway(service_ticket=staffapi_service_ticket)

    @classmethod
    async def init(cls, uow: UnitOfWork):
        return cls(
            uow=uow,
            staffapi_service_ticket=await get_tvm_service_ticket('staff-api'),
        )

    async def sync(self, chiefs_records: list[ChiefRecord] = None):
        """
        Вытаскиваем из staff-api все чифства по неуволенным сотрудникам,
        дропаем роли и создаём все заново
        """
        if not chiefs_records:
            chiefs_records = []
            all_staffapi_chiefs = self.staffapi_gateway.get_persons_by_chunks(
                fields=['id', 'chiefs.id'],
            )
            async for staffapi_chiefs_chunk in all_staffapi_chiefs:
                chiefs_records.extend(self.process_chunk(staffapi_chiefs_chunk))

        async with self.uow:
            await self.uow.person_relationships.drop_all_chiefs()
            await self.uow.person_relationships.bulk_create_chiefs(chiefs_records)

    @staticmethod
    def process_chunk(chiefs_chunk: list[dict]) -> list[ChiefRecord]:
        chiefs: dict[tuple[int, int], bool] = {}

        for item in chiefs_chunk:
            person_id = item['id']
            is_direct = True
            for chief in item['chiefs']:
                chiefs.setdefault((chief['id'], person_id), is_direct)
                is_direct = False

        return [
            (chief_id, person_id, is_direct)
            for (chief_id, person_id), is_direct in chiefs.items()
        ]


class StaffApiOrganizationSync:
    """
    Класс для синхронизации организаций из staff-api
    """
    def __init__(self, uow: UnitOfWork, staffapi_service_ticket):
        self.uow = uow
        self.staffapi_gateway = StaffApiGateway(service_ticket=staffapi_service_ticket)

    @classmethod
    async def init(cls, uow: UnitOfWork):
        return cls(
            uow=uow,
            staffapi_service_ticket=await get_tvm_service_ticket('staff-api'),
        )

    async def sync(self):
        counters = defaultdict(int)
        async with self.uow:
            async for staffapi_org_chunk in self.staffapi_gateway.get_organizations_by_chunks():
                added, updated, absent_in_staffapi = (
                    await self.process_chunk(staffapi_org_chunk)
                )
                counters['added'] += added
                counters['updated'] += updated
                counters['absent_in_staffapi'] += absent_in_staffapi

        logger.info(
            'sync_staffapi_organizations finished. added: %s, updated: %s, absent in staff-api: %s',
            counters['added'],
            counters['updated'],
            counters['absent_in_staffapi'],
        )

    async def process_chunk(self, organizations_data: list[dict]) -> tuple[int, int, int]:
        added = updated = absent_in_staffapi = 0
        first_id = organizations_data[0]['id']
        last_id = organizations_data[-1]['id']
        staffapi_companies = {
            company['id']: company
            for company in organizations_data
        }
        db_companies = {
            company.company_id: company
            for company in await self.uow.companies.get_for_staffapi_sync(first_id, last_id)
        }
        for company_id, company_data in staffapi_companies.items():
            db_company = db_companies.pop(company_id, None)
            if db_company:
                if db_company.name == company_data['name']:
                    continue
                logger.info(
                    'Updating name for company: id=%s, old_name=%s, new_name=%s',
                    company_id,
                    db_company.name,
                    company_data['name'],
                )
                await self.uow.companies.update(company_id=company_id, name=company_data['name'])
                updated += 1
            else:
                logger.info('Creating company: id=%s, name=%s', company_id, company_data['name'])
                await self.uow.companies.create(
                    company_id=company_id,
                    name=company_data['name'],
                    provider=Provider.aeroclub,
                    holding_id=settings.YANDEX_HOLDING_ID,
                )
                added += 1

        absent_in_staffapi = len(db_companies)
        for company_id, company in db_companies.items():
            logger.warning(
                'Company is not present in staff-api: id=%s, name=%s',
                company_id,
                company.name,
            )

        return added, updated, absent_in_staffapi
