import asyncio
import logging
from random import shuffle
from typing import Optional

from jinja2 import Template
from library.python import resource
import xmltodict

from intranet.trip.src.api.auth import get_tvm_service_ticket
from intranet.trip.src.config import settings
from intranet.trip.src.enums import Citizenship, DocumentType
from intranet.trip.src.lib.hub.api import Hub
from intranet.trip.src.lib.hub.enums import HubSyncStatus, ProfileSyncStatus
from intranet.trip.src.lib.hub.models import HubProfile
from intranet.trip.src.lib.utils import paginate
from intranet.trip.src.unit_of_work import UnitOfWork

logger = logging.getLogger(__name__)


HUB_DOCUMENT_TYPES = {
    DocumentType.passport,
    DocumentType.external_passport,
}


class HubException(Exception):
    pass


class HubProfileSyncBase:
    CHUNK_SIZE = 20
    STATUS_SYNC_DELAY = 10  # sec
    STATUS_SYNC_ATTEMPTS = 2 * int(CHUNK_SIZE / STATUS_SYNC_DELAY) + 1

    def __init__(self, uow: UnitOfWork, service_ticket: str):
        self.uow = uow
        self.api = Hub(
            host=settings.HUB_API_URL,
            timeout=settings.AEROCLUB_TIMEOUT,
            service_ticket=service_ticket,
        )
        path = 'intranet/trip/src/lib/hub/template.xml'
        self.batch_profiles_template = Template(resource.resfs_read(path).decode('utf-8'))

    @classmethod
    async def init(cls, uow: UnitOfWork):
        return cls(
            uow=uow,
            service_ticket=await get_tvm_service_ticket('zora'),
        )

    async def build_profiles(self, person_ids: list[int]) -> list[HubProfile]:
        raise NotImplementedError

    async def update_person_in_db(self, person_id: int, provider_profile_id: int):
        raise NotImplementedError

    async def update_aeroclub_ids_in_db(
            self,
            profiles: list[HubProfile],
            ext_uid_to_aeroclub_id: dict,
    ):
        ext_uid_to_person_id = {p.external_uid: p.person_id for p in profiles}
        for external_uid, provider_profile_id in ext_uid_to_aeroclub_id.items():
            person_id = ext_uid_to_person_id.get(external_uid)
            if not person_id:
                continue

            await self.uow.persons.update(
                person_id=person_id,
                provider_profile_id=provider_profile_id,
            )

    async def sync(self, person_ids: list[int]) -> dict[str, int]:
        logger.info('%s.sync. ids: %s', self.__class__.__name__, str(person_ids))

        profiles = await self.build_profiles(person_ids)
        logger.info('%s.build_profiles. profiles count: %d', self.__class__.__name__, len(profiles))

        ext_uid_to_aeroclub_id = {}
        for chunk in paginate(profiles, by=self.CHUNK_SIZE):
            data = await self._process_chunk(chunk)
            ext_uid_to_aeroclub_id.update(data)

        ext_uid_to_person_id = {p.external_uid: p.person_id for p in profiles}
        for external_uid, provider_profile_id in ext_uid_to_aeroclub_id.items():
            person_id = ext_uid_to_person_id.get(external_uid)
            if not person_id:
                continue

            await self.update_person_in_db(person_id, provider_profile_id)

        return ext_uid_to_aeroclub_id

    async def _process_chunk(self, profiles: list[HubProfile]) -> dict[str, int]:
        try:
            batch_id = await self.push_profiles(profiles)
        except Exception as exc:
            logger.exception(str(exc))
            return {}

        logger.info(
            'ProfileSync. Batch Id: %s. persons: %s',
            batch_id, ', '.join([p.login for p in profiles])
        )

        try:
            return await self.get_sync_result(batch_id)
        except Exception as ex:
            logger.exception(str(ex))
            return {}

    async def push_profiles(self, profiles: list[HubProfile]) -> str:
        shuffle(profiles)  # чтобы даже если ничего не поменялось не получить Fail
        data = self.batch_profiles_template.render(
            profiles=profiles,
            document_type=DocumentType,
            citizenship=Citizenship,
        )
        logger.info('%s.push_profiles count. %s', self.__class__.__name__, len(profiles))
        if len(profiles) == 1:
            response_text = await self.api.push_one_profile(data)
        else:
            response_text = await self.api.push_many_profiles(data)
        logger.info('%s.push_profiles. Response %s', self.__class__.__name__, response_text)
        resp_dict = xmltodict.parse(response_text)
        return resp_dict['profileSynchronizationResponse']['profileSynchronizationBatchId']

    def _process_sync_result(self, data: dict) -> dict:
        uid_to_profile = {}
        for profile in data['profileSynchronizationStatuses']:
            external_uid = profile['profile']['uniqueIdentifier']
            provider_profile_id = int(profile['profile']['profileId'])
            profile_sync_state = profile['profileSynchronizationState']

            if profile_sync_state not in ProfileSyncStatus.synced_statuses:
                logger.warning(
                    'ProfileSync. NOT synced: %s: %s',
                    external_uid, profile_sync_state,
                )
                continue

            uid_to_profile[external_uid] = provider_profile_id
            logger.info('ProfileSync. Synced: %s: %s', external_uid, provider_profile_id)
        return uid_to_profile

    async def get_sync_result(self, batch_id: str) -> dict[str, int]:
        """
        Дожидаемся полной обработки батча
        и возвращаем маппинг external_uid к provider_profile_id
        """
        for attempt in range(self.STATUS_SYNC_ATTEMPTS):
            resp_dict = await self.api.get_synchronization_status(batch_id)
            status = resp_dict['batchProfileSynchronizationStatus']

            if status == HubSyncStatus.FAILED:
                # вся пачка зафейлена, дальше статус не продвинется.
                raise HubException(f'Sync failed. Batch id: {batch_id} attempt: {attempt}')

            elif status == HubSyncStatus.IN_PROGRESS:
                # структура отправленного xml корректна, обработка пока не завершена
                logger.info(
                    '%s.get_sync_result Retrying. Attempt: %s',
                    self.__class__.__name__, attempt,
                )
                await asyncio.sleep(self.STATUS_SYNC_DELAY)

            elif status == HubSyncStatus.COMPLETED:
                # все профили из пачки обработаны корректно.
                uid_to_profile = self._process_sync_result(resp_dict)
                logger.info(
                    'ProfileSync. Sync finished batch_id: %s attempt: %s',
                    batch_id, attempt,
                )
                return uid_to_profile

            else:
                raise HubException(
                    f'Sync failed. Batch id: {batch_id} unknown sync status: {status}'
                )

        raise HubException(f'Sync failed. Batch id: {batch_id} maximum ({attempt}) attempts spent.')


class PersonHubProfileSync(HubProfileSyncBase):

    async def build_profiles(self, ids: list[int]):
        data = await self.uow.persons.get_for_ihub_sync(ids)
        profiles = []
        for item in data:
            item = dict(item)
            item['company_uid'] = item['company']['aeroclub_company_uid']
            item['first_name_ru'] = item['first_name_ac']
            item['last_name_ru'] = item['last_name_ac']
            item['middle_name_ru'] = item['middle_name_ac']
            item['first_name_en'] = item['first_name_ac_en']
            item['last_name_en'] = item['last_name_ac_en']
            item['middle_name_en'] = item['middle_name_ac_en']
            profiles.append(HubProfile(**item))
        return profiles

    async def update_person_in_db(self, person_id: int, provider_profile_id: int):
        await self.uow.persons.update(person_id=person_id, provider_profile_id=provider_profile_id)


class ExtPersonHubProfileSync(HubProfileSyncBase):

    async def build_profiles(self, ids: list[int]):
        data = await self.uow.ext_persons.get_for_ihub_sync(ids)
        profiles = []
        for item in data:
            item = dict(item)
            item['company_uid'] = 'yandex_oplata_lichnykh_poezdok'
            item['first_name_ru'] = item['first_name']
            item['last_name_ru'] = item['last_name']
            item['middle_name_ru'] = item['middle_name']
            item['login'] = ''
            profiles.append(HubProfile(**item))
        return profiles

    async def update_person_in_db(self, person_id: int, provider_profile_id: int):
        await self.uow.ext_persons.update(
            ext_person_id=person_id,
            provider_profile_id=provider_profile_id,
        )


async def sync_hub_profiles(
    uow: UnitOfWork,
    person_ids: Optional[list[int]] = None,
    ext_person_ids: Optional[list[int]] = None,
) -> dict[str, int]:
    if person_ids:
        hub_sync = await PersonHubProfileSync.init(uow)
        return await hub_sync.sync(person_ids)
    if ext_person_ids:
        hub_sync = await ExtPersonHubProfileSync.init(uow)
        return await hub_sync.sync(ext_person_ids)
