import aiohttp
import logging
import json
from typing import Optional
from tenacity import TryAgain

from pydantic.json import pydantic_encoder

from intranet.trip.src.config import settings
from intranet.trip.src.lib.aeroclub.exceptions import (
    AeroclubClientError,
    AeroclubBadDocument,
)
from intranet.trip.src.lib.provider import BaseServiceProviderClient
from intranet.trip.src.lib.aeroclub.models import ProfileDocumentIn


logger = logging.getLogger(__name__)


class BaseAeroclubClient(BaseServiceProviderClient):
    RETRY_CODES = {
        499,
        502,
        503,
        504,
    }

    def get_auth_headers(self, *args, **kwargs) -> dict:
        return {
            'Authorization': f'Bearer {settings.AEROCLUB_API_TOKEN}',
        }

    def get_session(self, timeout: int = None) -> aiohttp.ClientSession:
        timeout = aiohttp.ClientTimeout(total=timeout or self.timeout)
        headers = self.get_headers()
        headers.update(self.auth_headers)
        return aiohttp.ClientSession(
            timeout=timeout,
            headers=headers,
            # чтобы клиент aiohttp смог сдампить в json, например, даты
            json_serialize=lambda d: json.dumps(d, default=pydantic_encoder),
        )

    async def parse_response(self, response: aiohttp.ClientResponse, **kwargs) -> dict:
        if response.status in self.RETRY_CODES:
            raise TryAgain()
        try:
            response.raise_for_status()
        except aiohttp.ClientResponseError:
            message = (
                f'Got status: {response.status}, for request: '
                f'{response.method} {response.url}'
            )
            raise AeroclubClientError(content=message, status_code=response.status)

        try:
            data = await response.json()
        except aiohttp.ContentTypeError:
            # Некоторые ручки Аэроклуба (reserve, execute) возвращают 200 с пустым text/html
            logger.info(
                'Aeroclub endpoint response with not json. Status: %s, method: %s, url: %s',
                response.status, response.method, response.url,
            )
            await response.text()
            return {}

        if 'is_success' in data and not data['is_success']:
            logger.error('Bad response status from Aeroclub request: %s', data['errors'])
            msg = data['errors'][0] if data['errors'] else 'aeroclub error'
            if msg == 'Недопустимый тип документа':
                raise AeroclubBadDocument(msg)
            raise AeroclubClientError(content=msg, status_code=422)
        return data


class Aeroclub(BaseAeroclubClient):
    """
    https://beta-api.aeroclub.ru/api-docs#
    https://docs.aeroclub.ru/api
    https://yav.yandex-team.ru/edit/secret/sec-01ec9yaxpxsgza5k6ypdtzfnkr/explore/version/ver-01ecpzjqfarx6731pg4mrm1s6p
    """
    CLIENT_NAME = 'aeroclub'

    def get_headers(self):
        return {
            'Content-Type': 'application/json',
            'Accept': 'application/vnd.aeroclub-api.1.103+json',
        }

    # PROFILES
    async def get_profile(self, profile_id: Optional[int] = None) -> dict:
        """
        Получаем данные профиля по id или профиля текущего пользователя, если id не указан
        """
        path = 'profile'
        if profile_id:
            path += f'/{profile_id}'

        return await self._make_request(
            method='get',
            path=path,
        )

    # SEARCH
    async def create_search_request(self, data: dict) -> dict:
        """
        Создание поискового запроса
        """
        return await self._make_request(
            method='put',
            path='search',
            json=data,
        )

    async def get_search_request(self, request_id: int) -> dict:
        """
        Получение поискового запроса
        """
        return await self._make_request(
            method='get',
            path=f'search/{request_id}',
            params={},
        )

    async def get_search_results(self, request_id: int, params: dict) -> dict:
        """
        Получение результатов поискового запроса
        """
        return await self._make_request(
            method='get',
            path=f'search/{request_id}/results',
            params=params,
        )

    async def get_search_results_count(self, request_id: int, params: dict) -> dict:
        """
        Получение количества результатов поискового запроса
        """
        return await self._make_request(
            method='get',
            path=f'search/{request_id}/results/count',
            params=params,
        )

    async def get_search_option_results(
            self,
            request_id: int,
            option_id: int,
            params: dict,
    ) -> dict:
        """
        Получение результатов поискового запроса по одной опции
        """
        return await self._make_request(
            method='get',
            path=f'search/{request_id}/options/{option_id}/results',
            params=params,
        )

    async def get_search_option_results_count(
            self,
            request_id: int,
            option_id: int,
            params: dict,
    ) -> dict:
        """
        Получение количества результатов поискового запроса по одной опции
        """
        return await self._make_request(
            method='get',
            path=f'search/{request_id}/options/{option_id}/results/count',
            params=params,
        )

    async def get_search_option_details(
            self,
            request_id: int,
            option_id: int,
            key: str,
            detail_index: int = None,
    ) -> dict:
        """
        Получение детальной информации об одном результате поискового запроса
        """
        params = {}
        if detail_index is not None:
            params['detailIndex'] = detail_index
        return await self._make_request(
            method='get',
            path=f'search/{request_id}/options/{option_id}/details/{key}',
            params=params,
        )

    async def get_search_filters(self, request_id: int) -> list:
        """
        Получения доступных параметров фильтрации поискового запроса
        """
        return await self._make_request(
            method='get',
            path=f'search/{request_id}/filterparameters',
        )

    # TRIPS
    async def create_journey(self, data: dict) -> dict:
        """
        Создание групповой командирвоки с соответсвующими персональными командировками.
        Групповая командировка у нас - Trip, у Аэроклуба - Journey
        Персональная командировка у нас - PersonTrip, у Аэроклуба - BusinessTrip
        """
        return await self._make_request(
            method='put',
            path='journeys',
            json=data,
        )

    async def get_trip(self, journey_id: int, trip_id: int) -> dict:
        """
        Получение одной персональной командировки.
        Внутри командировки также есть список услуг.
        """
        return await self._make_request(
            method='get',
            path=f'journeys/{journey_id}/trips/{trip_id}',
        )

    async def get_trip_custom_properties(self, journey_id: int, trip_id: int) -> dict:
        """
        Получение списка доступных дополнительных данные командировки
        """
        return await self._make_request(
            method='get',
            path=f'journeys/{journey_id}/trips/{trip_id}/stage/fact/customproperties',
        )

    async def add_trip_custom_properties(
            self,
            journey_id: int,
            trip_id: int,
            id_properties: dict,
            text_properties: dict,
    ) -> dict:
        """
        Добавление дополнительных данных командировки
        """
        custom_properties = []
        for key, value in id_properties.items():
            custom_properties.append({
                'id': key,
                'list_value': {
                    'id': value,
                },
            })
        for key, value in text_properties.items():
            custom_properties.append({
                'id': key,
                'text_value': value,
            })
        return await self._make_request(
            method='post',
            path=f'journeys/{journey_id}/trips/{trip_id}/stage/fact/customproperties',
            json={
                'business_trip_custom_properties': [{
                    'custom_properties': custom_properties,
                }],
                'reason_codes': [],
            },
        )

    async def _get_authorization_state(self, journey_id: int, trip_id: int, stage: str) -> dict:
        return await self._make_request(
            method='get',
            path=f'journeys/{journey_id}/trips/{trip_id}/stage/{stage}/authfx/state',
        )

    async def get_trip_authorization_state(self, journey_id: int, trip_id: int) -> dict:
        """
        Получить статус авторизации факта поездки.
        После этого командировка переходит в статус authorization_status = 'Authorized',
        потому что у нас настроена автоматическая авторизация.
        """
        return await self._get_authorization_state(journey_id, trip_id, stage='fact')

    async def get_services_authorization_state(self, journey_id: int, trip_id: int) -> dict:
        """
        Получить статус авторизации услуг.
        После этого услуги переходят в статус authorization_status = 'Pending'
        """
        return await self._get_authorization_state(journey_id, trip_id, stage='services')

    async def send_services_authorization_request(self, journey_id: int, trip_id: int) -> dict:
        """
        Отправка запроса на авторизацию услуг.
        После этого услуги переходят в статус authorization_status = 'InProgress'
        """
        return await self._make_request(
            method='put',
            path=f'journeys/{journey_id}/trips/{trip_id}/stage/services/authfx/state',
            json={
                'state': 'authorize',
                'auto_execution_required': True,
            },
        )

    async def authorize_trip(self, token: str, profile_id: int) -> dict:
        """
        Авторизация командировки или услуг по токену.
        После этого услуги переходят в статус authorization_status = 'Authorized'
        """
        return await self._make_request(
            method='post',
            path=f'/authfx/trip/batch/{token}/complete',
            json={
                'comment': '',
                'profile_id': profile_id,
                'is_authorized': True,
            },
        )

    async def get_messages(
            self,
            journey_id: int,
            trip_id: int,
            last_message_id: int = None,
    ) -> list:
        """
        Получить последние сообщения
        """
        params = {}
        if last_message_id is not None:
            params['lastMessageID'] = last_message_id
        else:
            params['limit'] = settings.AEROCLUB_LIMIT_MESSAGES

        return await self._make_request(
            method='get',
            path=f'/journeys/{journey_id}/trips/{trip_id}/posts',
            params=params,
        )

    async def send_message(
            self,
            journey_id: int,
            trip_id: int,
            message: str,
            profile_id: int = None,
    ) -> dict:
        """
        Отправить сообщения в чат аэроклуба
        """
        data = {
            'message': message,
        }
        if profile_id:
            data['profile_id'] = profile_id

        return await self._make_request(
            method='put',
            path=f'/journeys/{journey_id}/trips/{trip_id}/posts',
            json=data,
        )

    # SERVICES
    async def add_service_to_trip(
            self,
            request_id: int,
            option_id: int,
            key: str,
            journey_id: int,
            trip_id: int,
            detail_index: Optional[int] = None,
    ) -> dict:
        """
        Добавление результата поиска в командировку
        После этого услуга получает свой идентификатор (order_id, service_id)
        и существует уже вне поискового запроса.
        """
        data = {
            'business_trip_numbers': [trip_id],
            'journey_number': journey_id,
        }
        if detail_index is not None:
            data['detail_index'] = detail_index
        return await self._make_request(
            method='put',
            path=f'search/{request_id}/options/{option_id}/details/{key}/selectToTrip',
            json=data,
        )

    async def get_service(self, order_id: int, service_id: int) -> dict:
        """
        Получение одной услуги
        """
        return await self._make_request(
            method='get',
            path=f'orders/{order_id}/services/{service_id}',
        )

    async def add_person_to_service(self, order_id: int, service_id: int, data: list[dict]) -> dict:
        """
        Добавление путешественников в услугу
        """
        return await self._make_request(
            method='put',
            path=f'orders/{order_id}/services/{service_id}/travellers',
            json=data,
        )

    async def get_service_custom_properties(
            self,
            journey_id: int,
            trip_id: int,
            order_id: int,
            service_id: int,
            is_additional: bool = False,
    ) -> dict:
        """
        Получение списка доступных дополнительных данных услуги
        """
        stage = 'additionalservice' if is_additional else 'services'
        return await self._make_request(
            method='get',
            path=f'journeys/{journey_id}/trips/{trip_id}/stage/{stage}/customproperties',
            params={
                'orderNumber': order_id,
                'serviceNumber': service_id,
            }
        )

    async def add_service_custom_properties(
            self,
            journey_id: int,
            trip_id: int,
            order_id: int,
            service_id: int,
            traveller_id: int,
            is_additional: bool = False,
    ) -> dict:
        stage = 'additionalservice' if is_additional else 'services'
        return await self._make_request(
            method='post',
            path=f'journeys/{journey_id}/trips/{trip_id}/stage/{stage}/customproperties',
            json={
                'order_number': order_id,
                'service_number': service_id,
                'business_trip_custom_properties': [{
                    'custom_properties': [],
                    'traveller': {'id': traveller_id},
                }],
                'reason_codes': [{
                    'traveller': {'id': traveller_id},
                    'reason_codes': [],
                }],
            }
        )

    async def get_additional_service_authorization_state(
            self,
            order_id: int,
            service_id: int,
    ) -> dict:
        return await self._make_request(
            method='get',
            path=f'orders/{order_id}/services/{service_id}/authfx/state',
        )

    async def send_additional_service_authorization_request(
            self,
            order_id: int,
            service_id: int,
    ) -> dict:
        return await self._make_request(
            method='put',
            path=f'orders/{order_id}/services/{service_id}/authfx/state',
            json={
                'state': 'authorize',
                'auto_execution_required': True,
            },
        )

    async def get_allowed_documents(
            self,
            order_id: int,
            service_id: int,
            profile_id: int = None,
    ) -> dict:
        """
        Получение доступных документов для оформления услуги
        """
        params = {}
        if profile_id:
            params['profileId'] = profile_id
        return await self._make_request(
            method='get',
            path=f'/orders/{order_id}/services/{service_id}/alloweddocuments',
            params=params,
        )

    async def get_free_seats(self, order_id: int, service_id: int) -> dict:
        """
        Получение актуальной информации о свободных местах (ЖД)
        """
        return await self._make_request(
            method='get',
            path=f'orders/{order_id}/services/{service_id}/seats',
        )

    async def reserve_service(
            self,
            order_id: int,
            service_id: int,
            rail_parameters: dict = None,
            auto_execution: bool = False,
    ) -> dict:
        """
        Запрос на бронирование услуги
        """
        data = {
            'auto_execution_required': auto_execution,
        }
        if rail_parameters:
            data['rail_parameters'] = rail_parameters
        return await self._make_request(
            method='put',
            path=f'orders/{order_id}/services/{service_id}/reserve',
            json=data,
        )

    async def execute_service(self, order_id: int, service_id: int) -> dict:
        """
        Офомрление услуги
        """
        return await self._make_request(
            method='put',
            path=f'orders/{order_id}/services/{service_id}/execute',
        )

    async def cancel_service(self, order_id: int, service_id: int) -> dict:
        """
        Запрос на отмену услуги
        """
        return await self._make_request(
            method='put',
            path=f'orders/{order_id}/services/{service_id}/cancel',
        )

    # REFERENCES
    async def get_references(self, query: str, type: str) -> dict:
        """
        Саджест по объектам Аэроклуба (города, страны, авиакомпании и т.п.)
        """
        result = await self._make_request(
            method='get',
            path='references',
            params={
                'query': query,
                'type': type,
            },
        )
        return result[:10]

    async def add_documents(self, profile_id: int, documents: list[ProfileDocumentIn]) -> dict:
        """
        Добавление документов (паспортов) пользователя

        Формат ответа:
        {'data': [909384, 909385], 'message': None, 'errors': None, 'is_success': True}

        Ограничения:
        - документ типа Passport можно добавить только один. Если он уже есть, то старый удалится.
        - в одном запросе нельзя передавать документы разных типов.
        """
        return await self._make_request(
            method='put',
            path='/integration/profile/documents',
            json={
                'profile_id': profile_id,
                'documents': documents,
            },
        )

    async def add_document(self, profile_id: int, document: ProfileDocumentIn) -> int:
        result = await self.add_documents(profile_id=profile_id, documents=[document])
        return result['data'][0]

    async def delete_document(self, profile_id: int, document_id: int) -> dict:
        """
        Удаление документа
        """
        return await self._make_request(
            method='delete',
            path=f'/profile/{profile_id}/documents/{document_id}',
        )


class AeroclubFileSystemReadClient(BaseServiceProviderClient):
    CLIENT_NAME = 'aeroclub_fs_read'
    RESPONSE_TYPE = 'read'

    def get_auth_headers(self, *args, **kwargs) -> dict:
        """
        Файлы доступны без авторизации
        """
        return {}

    async def get_file(self, path, params):
        return await self._make_request(
            method='get',
            path=path,
            params=params,
        )


class AeroclubFileSystemWriteClient(BaseAeroclubClient):
    CLIENT_NAME = 'aeroclub_fs_write'

    def get_headers(self):
        return {}

    async def send_file(
            self,
            journey_id: int,
            trip_id: int,
            content_type: str,
            file_name: str,
            file_data: bytes,
            profile_id: int = None,
    ) -> dict:
        """
        Отправить файл в чат аэроклуба
        """
        data = aiohttp.FormData()
        data.add_field('file', file_data, content_type=content_type, filename=file_name)

        if profile_id:
            data.add_field('profile_id', str(profile_id))

        headers = self.get_auth_headers()
        return await self._make_request(
            method='put',
            path=f'/journeys/{journey_id}/trips/{trip_id}/posts',
            headers=headers,
            data=data,
        )


aeroclub_fs = AeroclubFileSystemReadClient(
    host=settings.AEROCLUB_FS_URL,
    timeout=settings.AEROCLUB_TIMEOUT,
)

aeroclub = Aeroclub(
    host=settings.AEROCLUB_API_URL,
    timeout=settings.AEROCLUB_TIMEOUT,
)

aeroclub_fs_write = AeroclubFileSystemWriteClient(
    host=settings.AEROCLUB_API_URL,
    timeout=settings.AEROCLUB_TIMEOUT,
)
