from datetime import datetime as dt
from decimal import Decimal
import logging
import re
from typing import Union

from intranet.trip.src.api.schemas import AviaSearchFilter, HotelSearchFilter, RailSearchFilter
from intranet.trip.src.api.schemas.provider.search import (
    AviaDetailResponse,
    AviaLocation,
    AviaSearchInfo,
    AviaSearchResult,
    BaseFlight,
    BaseInfo,
    BaseTrain,
    FilterItem,
    FilterSelectValue,
    Flight,
    FlightBaggage,
    GeoPosition,
    Hotel,
    HotelDetail,
    HotelDetailResponse,
    HotelLocation,
    HotelSearchInfo,
    HotelSearchResult,
    Loc,
    ProviderLeg,
    ProviderSearchResultCount,
    ProviderSegment,
    RailDetailResponse,
    RailLocation,
    RailSearchInfo,
    RailSearchResult,
    Room,
    SearchAviaRequestIn,
    SearchHotelRequestIn,
    SearchRailRequestIn,
    Train,
    TrainCarriage,
    TrainCarriageDetail,
    TrainCarriagePlace,
    TransportBaggage,
)
from intranet.trip.src.config import settings
from intranet.trip.src.enums import (
    FilterValueType,
    ObjectType,
    SearchOrdering,
    SearchStatus,
    ServiceType,
)
from intranet.trip.src.lib import aeroclub
from intranet.trip.src.lib.aeroclub.enums import (
    ACServiceType,
    AviaRules,
    AviaTransfer,
    FlightBaggageType,
    HotelPaymentPlace,
    SearchRequestStatus,
    SearchResultsOrdering,
    Time,
    TrainCarriageClassOption,
    TrainCarriageType,
    TrainCategory,
    TrainCompartmentGender,
)
from intranet.trip.src.lib.aeroclub.models import SearchOptionIn, SearchRequestIn
from intranet.trip.src.lib.utils import b64json_to_dict, dict_to_b64json, get_by_lang, safe_getitem

from intranet.trip.src.logic.providers import BaseConverter

logger = logging.getLogger(__name__)
DATE_WITH_DAYS_PATTERN = re.compile(r'^\d+\.\d{2}:\d{2}:\d{2}$')


class AeroclubKeyConverterIn(BaseConverter):

    def convert(self, key_data: str) -> dict:
        key_decoded = b64json_to_dict(key_data)
        return {'key': key_decoded['key'], 'option_id': key_decoded['option_number']}


class BaseAeroclubSearchConverterOut(BaseConverter):

    @staticmethod
    def convert_id(data: dict) -> str:
        key_data = {'key': data['key'], 'option_number': data['option_number']}
        return dict_to_b64json(key_data)

    def convert_suggest(self, data: dict, object_type: ObjectType) -> BaseInfo:
        return BaseInfo(
            type=object_type,
            id=data.get('id') or data.get('code') or data.get('iata'),
            name=data.get('title') or data.get('name', {}).get(self.lang),
        )

    def convert_location(self, data: dict, object_type: ObjectType) -> Loc:
        return Loc(
            type=object_type,
            name=data.get('title') or data.get('name', {}).get(self.lang),
        )

    @staticmethod
    def convert_date_time(date_time: str) -> dt:
        return dt.strptime(date_time.strip('Z'), '%Y-%m-%dT%H:%M:%S')

    @staticmethod
    def convert_travel_policy_violations(carriage_data: dict) -> list:
        return list({violation.strip() for violation in carriage_data['travel_policy_violations']})

    def convert(self, *args, **kwargs):
        raise NotImplementedError


class BaseAeroclubTransportSearchConverterOut(BaseAeroclubSearchConverterOut):

    @staticmethod
    def convert_trip_duration(trip_duration: str) -> int:
        if re.match(DATE_WITH_DAYS_PATTERN, trip_duration):
            duration = dt.strptime(trip_duration, '%d.%H:%M:%S')
            total_minutes = duration.day * 24 * 60 + duration.hour * 60 + duration.minute
        else:
            duration = dt.strptime(trip_duration, '%H:%M:%S')
            total_minutes = duration.hour * 60 + duration.minute
        return total_minutes

    @staticmethod
    def convert_timezone_offset(timezone_offset: str) -> int:
        # TODO: часовые пояса бывают и с минутами
        return int(timezone_offset.split(':')[0])

    def convert(self, *args, **kwargs):
        raise NotImplementedError


class BaseAeroclubSearchInfoConverterOut(BaseAeroclubSearchConverterOut):

    @staticmethod
    def convert_status(status: SearchRequestStatus) -> SearchStatus:
        map_status = {
            SearchRequestStatus.in_progress: SearchStatus.in_progress,
            SearchRequestStatus.undefined: SearchStatus.in_progress,
            SearchRequestStatus.pending: SearchStatus.in_progress,
            SearchRequestStatus.completed: SearchStatus.completed,
            SearchRequestStatus.search_response_error: SearchStatus.error,
        }
        return map_status[status]

    def convert(self, *args, **kwargs):
        raise NotImplementedError


class BaseAeroclubSearchFiltersConverterOut(BaseConverter):

    CONVERT_PARAMETER_NAME: dict[str, str] = None

    def convert_parameter_name(self, orig_name: str) -> str:
        if self.CONVERT_PARAMETER_NAME is None:
            return orig_name
        return self.CONVERT_PARAMETER_NAME.get(orig_name, orig_name)

    @staticmethod
    def convert_parameter_value(value: str, param_name: str):
        param_value = {
            'train_categories': TrainCategory,
            'carriage_types': TrainCarriageType,
        }
        return param_value[param_name](value).name if param_name in param_value else value

    def convert_multiselect_value(self, value: dict, param_name: str) -> FilterSelectValue:
        return FilterSelectValue(
            target_id=self.convert_parameter_value(value['code'], param_name),
            caption=get_by_lang(value['name'], self.lang),
        )

    def convert_multiselect_filter(self, item: dict) -> FilterItem:
        param_name = self.convert_parameter_name(item['parameter_name'])
        return FilterItem(
            name=param_name,
            type=FilterValueType.multiselect,
            values=[self.convert_multiselect_value(value, param_name) for value in item['values']],
        )

    def get_static_filters(self):
        return []

    def get_order_by_values(self) -> list[FilterSelectValue]:
        return [
            FilterSelectValue(
                target_id=SearchOrdering.price,
                caption={
                    'ru': 'По цене',
                    'en': 'By price',
                }[self.lang],
            ),
        ]

    def convert(self, data: dict) -> list[FilterItem]:
        return self.get_static_filters() + [
            self.convert_multiselect_filter(item) for item in data.get('data', [])
        ]


class BaseAeroclubSearchFiltersConverterIn(BaseConverter):

    @staticmethod
    def convert_travel_policy(is_restricted):
        if is_restricted is not None:
            is_restricted = not is_restricted
        return is_restricted

    @staticmethod
    def get_sort_field(order_by: SearchOrdering) -> SearchResultsOrdering:
        map_order_by = {
            SearchOrdering.price: SearchResultsOrdering.price,
            SearchOrdering.duration: SearchResultsOrdering.duration,
            SearchOrdering.departure_time: SearchResultsOrdering.by_time,
            SearchOrdering.arrival_time: SearchResultsOrdering.by_time,
        }
        if order_by in map_order_by:
            return map_order_by[order_by]
        return SearchResultsOrdering.optimal

    def convert(self, *args, **kwargs):
        raise NotImplementedError


class BaseAeroclubTransportSearchFiltersConverterOut(BaseAeroclubSearchFiltersConverterOut):

    def get_timespan_values(self):
        return [
            FilterSelectValue(
                target_id=Time.morning,
                caption={
                    'ru': 'Утро',
                    'en': 'Morning',
                }[self.lang],
            ),
            FilterSelectValue(
                target_id=Time.afternoon,
                caption={
                    'ru': 'День',
                    'en': 'Afternoon',
                }[self.lang],
            ),
            FilterSelectValue(
                target_id=Time.evening,
                caption={
                    'ru': 'Вечер',
                    'en': 'Evening',
                }[self.lang],
            ),
            FilterSelectValue(
                target_id=Time.night,
                caption={
                    'ru': 'Ночь',
                    'en': 'Night',
                }[self.lang],
            ),
        ]

    def get_order_by_values(self) -> list[FilterSelectValue]:
        return [
            FilterSelectValue(
                target_id=SearchOrdering.price,
                caption={
                    'ru': 'По цене',
                    'en': 'By price',
                }[self.lang],
            ),
            FilterSelectValue(
                target_id=SearchOrdering.duration,
                caption={
                    'ru': 'По длительности',
                    'en': 'By duration',
                }[self.lang],
            ),
            FilterSelectValue(
                target_id=SearchOrdering.departure_time,
                caption={
                    'ru': 'По времени начала',
                    'en': 'By time',
                }[self.lang],
            ),
        ]


class BaseAeroclubSearchCountConverterOut(BaseConverter):

    service_type = None

    def convert(self, data: dict) -> ProviderSearchResultCount:
        assert self.service_type is not None
        return ProviderSearchResultCount(
            service_type=self.service_type,
            count=data['data']['count'],
        )


class AeroclubAviaSearchFiltersConverterIn(BaseAeroclubSearchFiltersConverterIn):
    # TODO: неподходящее название класса

    @staticmethod
    def get_transfer(uf: AviaSearchFilter) -> list[AviaTransfer]:
        transfer = [AviaTransfer.direct]
        if uf.without_transfer:
            return transfer
        if uf.maximum_transfers_count is None or uf.maximum_transfers_count > 1:
            return None
        if uf.maximum_transfers_count > 0:
            transfer.append(AviaTransfer.one)
        return transfer

    @staticmethod
    def get_rules(uf: AviaSearchFilter) -> list[AviaRules]:
        rules = []
        if uf.is_changeable:
            rules.append(AviaRules.changeable)
        if uf.is_refundable:
            rules.append(AviaRules.refundable)
        return rules

    def convert(self, uf: AviaSearchFilter) -> aeroclub.SearchFilter:
        """
        Convert universal filter object to Aeroclub SearchFilter

        :param uf: AviaSearchFilter - universal filter object
        :return: SearchFilter - aeroclub filter object
        """
        transfer = self.get_transfer(uf)
        rules = self.get_rules(uf)

        return aeroclub.SearchFilter(
            orderBy=self.get_sort_field(uf.order_by),
            isDescending=uf.is_descending,
            IsTravelPolicyCompliant=self.convert_travel_policy(uf.is_restricted_by_travel_policy),
            aviaFilter_airCompanies=uf.air_companies,
            aviaFilter_classes=uf.cabin_classes,
            aviaFilter_transfer=transfer,
            aviaFilter_baggage=uf.has_baggage,
            departureAtThereTimes=uf.departure_there_timespan,
            arrivalAtThereTimes=uf.arrival_there_timespan,
            departureAtBackTimes=uf.departure_back_timespan,
            arrivalAtBackTimes=uf.arrival_back_timespan,
            departureFromThere=uf.departure_from_there,
            arrivalToThere=uf.arrival_to_there,
            departureFromBack=uf.departure_from_back,
            arrivalToBack=uf.arrival_to_back,
            aviaFilter_rules=rules,
        )


class AeroclubAviaSearchFiltersConverterOut(BaseAeroclubTransportSearchFiltersConverterOut):

    CONVERT_PARAMETER_NAME = {
        'aircompany': 'air_companies',
        'departurefromthere': 'departure_from_there',
        'arrivaltothere': 'arrival_to_there',
        'departurefromback': 'departure_from_back',
        'arrivaltoback': 'arrival_to_back',
        'cabinclass': 'cabin_classes',
    }

    def get_static_filters(self) -> list[FilterItem]:
        timespan_values = self.get_timespan_values()
        return [
            FilterItem(name='without_transfer', type=FilterValueType.boolean),
            FilterItem(name='has_baggage', type=FilterValueType.boolean),
            FilterItem(name='is_restricted_by_travel_policy', type=FilterValueType.boolean),
            FilterItem(name='is_changeable', type=FilterValueType.boolean),
            FilterItem(name='is_refundable', type=FilterValueType.boolean),
            FilterItem(
                name='departure_there_timespan',
                type=FilterValueType.multiselect,
                values=timespan_values,
            ),
            FilterItem(
                name='arrival_there_timespan',
                type=FilterValueType.multiselect,
                values=timespan_values,
            ),
            FilterItem(
                name='departure_back_timespan',
                type=FilterValueType.multiselect,
                values=timespan_values,
            ),
            FilterItem(
                name='arrival_back_timespan',
                type=FilterValueType.multiselect,
                values=timespan_values,
            ),
            FilterItem(
                name='order_by',
                type=FilterValueType.select,
                values=self.get_order_by_values(),
            ),
            FilterItem(
                name='is_descending',
                type=FilterValueType.boolean,
                values=None,
            ),
        ]


class AeroclubAviaSearchRequestConverterIn(BaseConverter):

    def convert(self, search_request_in: SearchAviaRequestIn) -> SearchRequestIn:
        return SearchRequestIn(
            profile_id=settings.AEROCLUB_PROFILE_ID,
            departure_city_id=search_request_in.from_id,
            arrival_city_id=search_request_in.to_id,
            departure_on=search_request_in.departure_on,
            round_trip_departure_on=search_request_in.departure_back_on,
            options=[
                SearchOptionIn(
                    service_type=ACServiceType.avia,
                    comment='',
                    departure_city_id=search_request_in.from_id,
                    arrival_city_id=search_request_in.to_id,
                    departure_on=search_request_in.departure_on,
                    number=1,
                    round_trip_departure_on=search_request_in.departure_back_on,
                ),
            ],
        )


class AeroclubAviaSearchCountConverterOut(BaseAeroclubSearchCountConverterOut):

    service_type = ServiceType.avia


class AeroclubAviaSearchConverterOut(BaseAeroclubTransportSearchConverterOut):
    """
    aeroclub /search/{request_id}/options/{option_id}/results response -> trip /search response
    """
    def convert_search_location(self, location_data: dict) -> AviaLocation:
        return AviaLocation(
            city=self.convert_location(location_data, ObjectType.city),
            country=self.convert_location(location_data['country'], ObjectType.country),
        )

    def convert_trip_location(self, trip_location_data: dict) -> AviaLocation:
        return AviaLocation(
            terminal=trip_location_data['terminal'] and Loc(
                name=trip_location_data['terminal'],
                type=ObjectType.terminal,
            ),
            airport=self.convert_location(
                data=trip_location_data,
                object_type=ObjectType.airport,
            ),
            city=self.convert_location(
                data=trip_location_data['city'],
                object_type=ObjectType.city,
            ),
            country=self.convert_location(
                data=trip_location_data['city']['country'],
                object_type=ObjectType.country,
            ),
        )

    def convert_search_info(self, data: list) -> Union[AviaSearchInfo, None]:
        """
        Информацию о поиске берем из первого результата поиска
        """
        # Если результатов поиска нет, информации о поиске тоже не будет
        if not data:
            return None
        search_segment = safe_getitem(data, [0, 'legs', 0], {})
        departure_back_on = safe_getitem(data, [0, 'legs', 1, 'departure_at'])
        if departure_back_on:
            departure_back_on = self.convert_date_time(departure_back_on).date()

        return AviaSearchInfo(
            location_from=self.convert_search_location(
                location_data=search_segment['departure_from']['city'],
            ),
            location_to=self.convert_search_location(
                location_data=search_segment['arrival_to']['city'],
            ),
            departure_on=self.convert_date_time(search_segment['departure_at']).date(),
            departure_back_on=departure_back_on,
            status=SearchStatus.completed,
        )

    @staticmethod
    def convert_is_refundable(mini_rules_data: list[dict]) -> bool | None:
        for mini_rule in mini_rules_data:
            if mini_rule['category'] == "Refundable":
                is_empty_rule = mini_rule.get('is_empty_rule')
                if is_empty_rule is None:
                    return None
                return not is_empty_rule
        return False

    @staticmethod
    def convert_is_changeable(mini_rules_data: list[dict]) -> bool | None:
        for mini_rule in mini_rules_data:
            if mini_rule['category'] == "Changeable":
                is_empty_rule = mini_rule.get('is_empty_rule')
                if is_empty_rule is None:
                    return None
                return not is_empty_rule
        return False

    @staticmethod
    def convert_baggage(segment_data: dict) -> FlightBaggage:
        baggage_in = segment_data['baggage']
        if baggage_in['type'] == FlightBaggageType.undefined:
            return FlightBaggage()

        base_baggage = {
            'quantity': baggage_in['quantity'] or 0,
            'weight': baggage_in['weight_measurement'] or 0,
        }

        return FlightBaggage(baggage=TransportBaggage(**base_baggage))

    def convert_segment(self, leg_data: dict) -> ProviderSegment:
        # ак не умеет все сегменты сразу отдавать
        # по этому отдаем один сегмент и количество пересадок
        baggage = self.convert_baggage(leg_data)
        return ProviderSegment(
            departure=self.convert_trip_location(
                trip_location_data=leg_data['departure_from'],
            ),
            departure_at=self.convert_date_time(leg_data['departure_at']),
            departure_at_utc=self.convert_date_time(leg_data['departure_at_utc']),
            departure_at_timezone_offset=self.convert_timezone_offset(
                timezone_offset=leg_data['departure_from']['city']['time_zone_offset'],
            ),
            arrival=self.convert_trip_location(trip_location_data=leg_data['arrival_to']),
            arrival_at=self.convert_date_time(leg_data['arrival_at']),
            arrival_at_utc=self.convert_date_time(leg_data['arrival_at_utc']),
            arrival_at_timezone_offset=self.convert_timezone_offset(
                timezone_offset=leg_data['arrival_to']['city']['time_zone_offset'],
            ),
            seats=leg_data['seats_available'],
            flight_number=safe_getitem(leg_data, ['segments', 0, 'flight_number']),
            route_duration=self.convert_trip_duration(leg_data['duration']),
            baggage=baggage,
            flight_class=leg_data['cabin_class'],
            carrier=self.convert_suggest(
                data=safe_getitem(leg_data, ['segments', 0, 'marketing_airline']),
                object_type=ObjectType.carrier,
            ),
            fare_code=safe_getitem(leg_data, ['segments', 0, 'fare_basis']),
        )

    def convert_leg(self, leg_data: dict) -> ProviderLeg:
        return ProviderLeg(
            segments=[
                self.convert_segment(leg_data)
            ],
            segments_count=len(leg_data['segments']),
            route_duration=self.convert_trip_duration(leg_data['duration']),
        )

    def convert_flight(self, flight_data: dict) -> Flight:
        return Flight(
            id=self.convert_id(flight_data),
            price=flight_data['tariff_total'],
            is_refundable=self.convert_is_refundable(flight_data['mini_rules']),
            is_changeable=self.convert_is_changeable(flight_data['mini_rules']),
            is_travel_policy_compliant=flight_data.get('is_travel_policy_compliant', True),
            legs=[self.convert_leg(leg_data) for leg_data in flight_data['legs']],
        )

    def convert(self, data: dict) -> AviaSearchResult:
        return AviaSearchResult(
            total=data['items_count'],
            page=data['page_number'] + 1,
            limit=data['items_per_page'],
            data=[self.convert_flight(flight) for flight in data['data']],
        )


class AeroclubAviaSearchInfoConverterOut(BaseAeroclubSearchInfoConverterOut):
    """
    aeroclub /search/{request_id} response -> trip /search/avia/{search_id}/info response
    """
    def convert_search_location(self, location_data: dict) -> AviaLocation:
        # У АК можем искать только по городам
        return AviaLocation(
            city=self.convert_location(location_data, ObjectType.city),
            country=self.convert_location(location_data['located_in'], ObjectType.country),
        )

    def convert(self, data: dict) -> AviaSearchInfo:
        departure_back_on = None
        first_option = data['data']['options'][0]
        if first_option['round_trip_departure_on']:
            departure_back_on = self.convert_date_time(
                date_time=first_option['round_trip_departure_on'],
            ).date()
        return AviaSearchInfo(
            location_from=self.convert_search_location(
                location_data=data['data']['departure_city'],
            ),
            location_to=self.convert_search_location(
                location_data=data['data']['arrival_city'],
            ),
            departure_on=self.convert_date_time(data['data']['departure_on']).date(),
            departure_back_on=departure_back_on,
            status=self.convert_status(data['data']['status']),
        )


class AeroClubAviaDetailConverterOut(AeroclubAviaSearchConverterOut):

    def convert_segment(self, leg_data: dict) -> ProviderSegment:
        segment = safe_getitem(leg_data, ['segments', 0], {})
        baggage = self.convert_baggage(segment)
        return ProviderSegment(
            departure=self.convert_trip_location(trip_location_data=leg_data['departure_from']),
            departure_at=self.convert_date_time(leg_data['departure_at']),
            departure_at_utc=self.convert_date_time(leg_data['departure_at_utc']),
            departure_at_timezone_offset=self.convert_timezone_offset(
                leg_data['departure_from']['city']['time_zone_offset'],
            ),
            arrival=self.convert_trip_location(trip_location_data=leg_data['arrival_to']),
            arrival_at=self.convert_date_time(leg_data['arrival_at']),
            arrival_at_utc=self.convert_date_time(leg_data['arrival_at_utc']),
            arrival_at_timezone_offset=self.convert_timezone_offset(
                leg_data['arrival_to']['city']['time_zone_offset'],
            ),
            seats=segment.get('seats_available'),
            flight_number=segment.get('flight_number'),
            route_duration=self.convert_trip_duration(segment['duration']),
            baggage=baggage,
            flight_class=segment['cabin_class'],
            carrier=self.convert_suggest(
                data=segment['marketing_airline'],
                object_type=ObjectType.carrier,
            ),
            aircraft=self.convert_suggest(
                data=segment['aircraft'],
                object_type=ObjectType.aircraft,
            ),
            fare_code=safe_getitem(leg_data, ['segments', 0, 'fare_basis']),
        )

    def convert_flight(self, flight_data: dict) -> BaseFlight:
        return BaseFlight(
            price=flight_data['tariff_total'],
            is_refundable=self.convert_is_refundable(flight_data['mini_rules']),
            is_changeable=self.convert_is_changeable(flight_data['mini_rules']),
            is_travel_policy_compliant=flight_data.get('is_travel_policy_compliant', True),
        )

    def convert(self, data: dict) -> AviaDetailResponse:
        return AviaDetailResponse(
            flight=self.convert_flight(data['data']),
            data=[self.convert_leg(leg) for leg in data['data']['legs']],
        )


class AeroclubHotelSearchFiltersConverterIn(BaseAeroclubSearchFiltersConverterIn):
    # TODO: неподходящее название класса
    @staticmethod
    def get_sort_field(order_by: SearchOrdering) -> SearchResultsOrdering:
        if order_by == SearchOrdering.price:
            return SearchResultsOrdering.price
        return SearchResultsOrdering.optimal

    def convert(self, uf: HotelSearchFilter) -> aeroclub.SearchFilter:
        return aeroclub.SearchFilter(
            orderBy=self.get_sort_field(uf.order_by),
            isDescending=uf.is_descending,
            hotelFilter_stars=uf.stars,
            hotelFilter_priceFrom=uf.price_from,
            hotelFilter_priceTo=uf.price_to,
            hotelFilter_hotelTypes=uf.hotel_types,
            IsTravelPolicyCompliant=self.convert_travel_policy(uf.is_restricted_by_travel_policy),
            hotelFilter_paymentPlaces=[HotelPaymentPlace.agency],  # Без отелей с оплатой на месте
            hotelFilter_confirmationTypes=uf.confirmation_types,
            hotelFilter_isRecommended=uf.is_recommended,
            hotelName=uf.hotel_name,
        )


class AeroclubHotelSearchFiltersConverterOut(BaseAeroclubSearchFiltersConverterOut):

    CONVERT_PARAMETER_NAME = {
        'hoteltypes': 'hotel_types',
        'confirmationtype': 'confirmation_type',
        'pricerange': 'price_range',
    }

    def get_static_filters(self) -> list[FilterItem]:
        stars = [
            FilterSelectValue(
                target_id='1',
                caption='1',
            ),
            FilterSelectValue(
                target_id='2',
                caption='2',
            ),
            FilterSelectValue(
                target_id='3',
                caption='3',
            ),
            FilterSelectValue(
                target_id='4',
                caption='4',
            ),
            FilterSelectValue(
                target_id='5',
                caption='5',
            ),
        ]
        payment_places = [
            FilterSelectValue(
                target_id=HotelPaymentPlace.check_in,
                caption={
                    'ru': 'При регистрации',
                    'en': 'During check-in',
                }[self.lang],
            ),
            FilterSelectValue(
                target_id=HotelPaymentPlace.agency,
                caption={
                    'ru': 'Агентством',
                    'en': 'By agency',
                }[self.lang],
            ),
        ]
        return [
            FilterItem(name='is_travel_policy_compliant', type=FilterValueType.boolean),
            FilterItem(name='is_recommended', type=FilterValueType.boolean),
            FilterItem(name='hotel_name', type=FilterValueType.string),
            FilterItem(name='stars', type=FilterValueType.multiselect, values=stars),
            FilterItem(
                name='payment_places',
                type=FilterValueType.multiselect, values=payment_places,
            ),
            FilterItem(
                name='order_by',
                type=FilterValueType.select,
                values=self.get_order_by_values(),
            ),
            FilterItem(
                name='is_descending',
                type=FilterValueType.boolean,
                values=None,
            ),
        ]


class AeroclubHotelSearchRequestConverterIn(BaseConverter):

    def convert(self, search_request_in: SearchHotelRequestIn) -> SearchRequestIn:
        return SearchRequestIn(
            profile_id=settings.AEROCLUB_PROFILE_ID,
            departure_city_id=-1,
            arrival_city_id=search_request_in.target_id,
            departure_on=search_request_in.check_in_on,
            round_trip_departure_on=search_request_in.check_out_on,
            options=[
                SearchOptionIn(
                    service_type=ACServiceType.hotel,
                    comment='',
                    arrival_city_id=search_request_in.target_id,
                    departure_on=search_request_in.check_in_on,
                    number=1,
                    search_mode=search_request_in.search_mode,
                    round_trip_departure_on=search_request_in.check_out_on,
                    checkin_on=search_request_in.check_in_on,
                    checkout_on=search_request_in.check_out_on,
                ),
            ],
        )


class AeroclubHotelSearchCountConverterOut(BaseAeroclubSearchCountConverterOut):
    """
    aeroclub /search/{request_id}/results/count response
     -> trip /search/hotel/{search_id}/count response
    """
    service_type = ServiceType.hotel


class AeroclubHotelSearchConverterOut(BaseAeroclubSearchConverterOut):
    """
    aeroclub /search/{request_id}/options/{option_id}/results response -> trip /search response
    """
    @staticmethod
    def get_min_price_per_night(tariffs: list[dict]) -> Decimal:
        if not tariffs:
            return Decimal('0')
        return min(
            Decimal(str(it['price_per_night']))
            for it in tariffs
        )

    def convert_hotel_location(self, data: dict):
        return HotelLocation(
            city=self.convert_location(data['city'], ObjectType.city),
            country=self.convert_location(data['city']['country'], ObjectType.country),
        )

    def convert_hotel(self, data: dict) -> Hotel:
        geo = data['geo_position']
        return Hotel(
            id=self.convert_id(data),
            hotel_name=get_by_lang(data['hotel']['name'], lang=self.lang),
            description=get_by_lang(data['description'], lang=self.lang),
            stars=data['stars'],
            image_url=data['image_url'],
            currency=data['currency'],
            min_price_per_night=self.get_min_price_per_night(data['tariffs']),
            address=get_by_lang(data['address'], lang=self.lang),
            geo_position=GeoPosition(
                latitude=geo['latitude'],
                longitude=geo['longitude'],
            ),
            location=self.convert_hotel_location(data),
            is_recommended=data['is_recommended'],
        )

    def convert(self, data: dict) -> HotelSearchResult:
        return HotelSearchResult(
            total=data['items_count'],
            page=data['page_number'] + 1,
            limit=data['items_per_page'],
            data=[
                self.convert_hotel(hotel)
                for hotel in data['data']
            ],
        )


class AeroclubHotelSearchInfoConverterOut(BaseAeroclubSearchInfoConverterOut):
    """
    aeroclub /search/{request_id} response -> trip /search/avia/{search_id}/info response
    """
    def convert_hotel_location(self, data: dict):
        return HotelLocation(
            city=self.convert_location(
                data=data['data']['options'][0]['arrival_city'],
                object_type=ObjectType.city,
            ),
            country=self.convert_location(
                data=data['data']['options'][0]['arrival_city']['located_in'],
                object_type=ObjectType.country,
            ),
        )

    def convert(self, data: dict) -> HotelSearchInfo:
        option = data['data']['options'][0]

        return HotelSearchInfo(
            check_in=option['checkin_on'][:10],
            check_out=option['checkout_on'][:10],
            location=self.convert_hotel_location(data),
            status=self.convert_status(data['data']['status']),
        )


class AeroclubHotelDetailConverterOut(BaseConverter):

    @staticmethod
    def convert_meals(data: dict) -> tuple[bool, list[str]]:
        is_meal_included = False
        names = []
        for meal in data:
            if meal['is_included_in_price']:
                is_meal_included = True
                names.append(meal['name'])
        return is_meal_included, names

    def convert_room(self, data: dict) -> Room:
        is_meal_included, meal_names = self.convert_meals(data['meals'])
        data['room_description']['ru'] = data['room_description']['russian']  # XXX
        data['room_description']['en'] = data['room_description']['english']  # XXX
        return Room(
            index=data['index'],
            images=data['images'],
            description=get_by_lang(data['room_description'], lang=self.lang),
            name=data['room_name'],
            is_meal_included=is_meal_included,
            meal_names=meal_names,
            is_travel_policy_compliant=data['is_travel_policy_compliant'],
            is_booking_by_request=data['sale_type'] != 'FreeSale',
            currency=data['currency'],
            price_total=data['tariff_total'],
            price_per_night=data['tariff_per_night'],
        )

    def convert_hotel(self, data: dict) -> HotelDetail:
        geo = data['geo_position']
        city = data['city']
        check_in = dt.fromisoformat(data['start_on'])
        check_out = dt.fromisoformat(data['end_on'])
        return HotelDetail(
            hotel_name=get_by_lang(data['hotel']['name'], lang=self.lang),
            stars=data['stars'],
            images=[it['url'] for it in data['hotel_images']],
            address=get_by_lang(data['address'], lang=self.lang),
            geo_position=GeoPosition(
                latitude=geo['latitude'],
                longitude=geo['longitude'],
            ),
            location=HotelLocation(
                city=Loc(
                    name=get_by_lang(city['name'], lang=self.lang),
                    type='city',
                ),
                country=Loc(
                    name=get_by_lang(city['country']['name'], lang=self.lang),
                    type='country',
                ),
            ),
            website=data['web_site'],
            is_recommended=data['is_recommended'],
            check_in=check_in,
            check_out=check_out,
            num_of_nights=(check_out - check_in).days,
        )

    def convert(self, data: dict) -> HotelDetailResponse:
        data = data['data']
        return HotelDetailResponse(
            hotel=self.convert_hotel(data),
            data=[self.convert_room(it) for it in data['rooms']],
        )


class AeroclubRailSearchFiltersConverterIn(BaseAeroclubSearchFiltersConverterIn):
    # TODO: неподходящее название класса
    @staticmethod
    def convert_carriage_types(uf: RailSearchFilter) -> Union[list[TrainCarriageType], None]:
        return (
            uf.carriage_types
            and [TrainCarriageType[carriage_type] for carriage_type in uf.carriage_types]
        )

    @staticmethod
    def convert_train_categories(uf: RailSearchFilter) -> list[TrainCategory]:
        return (
            uf.train_categories
            and [TrainCategory[train_category] for train_category in uf.train_categories]
        )

    def convert(self, uf: RailSearchFilter) -> aeroclub.SearchFilter:
        """
        Convert universal filter object to Aeroclub SearchFilter

        :param uf: RailSearchFilter - universal filter object
        :return: SearchFilter - aeroclub filter object
        """
        return aeroclub.SearchFilter(
            orderBy=self.get_sort_field(uf.order_by),
            isDescending=uf.is_descending,
            IsTravelPolicyCompliant=self.convert_travel_policy(uf.is_restricted_by_travel_policy),
            departureAtThereTimes=uf.departure_there_timespan,
            arrivalAtThereTimes=uf.arrival_there_timespan,
            departureFromThere=uf.departure_from_there,
            arrivalToThere=uf.arrival_to_there,
            railFilter_carriageTypes=self.convert_carriage_types(uf),
            railFilter_trainName=uf.train_names,
            railFilter_trainCategories=self.convert_train_categories(uf),
            railFilter_electronicRegistration=uf.has_electronic_registration,
            railFilter_carriageOwners=uf.carriage_owners,
            railFilter_branded=uf.is_brand_train,
        )


class AeroclubRailSearchFiltersConverterOut(BaseAeroclubTransportSearchFiltersConverterOut):

    CONVERT_PARAMETER_NAME = {
        'traincarriageowner': 'carriers',
        'carriagetype': 'carriage_types',
        'traincategory': 'train_categories',
        'trainname': 'train_names',
        'departurefromthere': 'departure_from_there',
        'arrivaltothere': 'arrival_to_there',
    }

    def get_static_filters(self) -> list[FilterItem]:
        timespan_values = self.get_timespan_values()
        return [
            FilterItem(
                name='departure_there_timespan',
                type=FilterValueType.multiselect,
                values=timespan_values,
            ),
            FilterItem(
                name='arrival_there_timespan',
                type=FilterValueType.multiselect,
                values=timespan_values,
            ),
            FilterItem(
                name='order_by',
                type=FilterValueType.select,
                values=self.get_order_by_values(),
            ),
            FilterItem(
                name='is_descending',
                type=FilterValueType.boolean,
                values=None,
            ),
        ]


class AeroclubRailSearchRequestConverterIn(BaseConverter):

    def convert(self, search_request_in: SearchRailRequestIn) -> SearchRequestIn:
        return SearchRequestIn(
            profile_id=settings.AEROCLUB_PROFILE_ID,
            departure_city_id=search_request_in.from_id,
            arrival_city_id=search_request_in.to_id,
            departure_on=search_request_in.departure_on,
            options=[
                SearchOptionIn(
                    service_type=ACServiceType.rail,
                    comment='',
                    departure_city_id=search_request_in.from_id,
                    arrival_city_id=search_request_in.to_id,
                    departure_on=search_request_in.departure_on,
                    number=1,
                ),
            ],
        )


class AeroclubRailSearchCountConverterOut(BaseAeroclubSearchCountConverterOut):
    """
    aeroclub /search/{request_id}/results/count response
     -> trip /search/rail/{search_id}/count response
    """
    service_type = ServiceType.rail


class AeroclubRailSearchConverterOut(BaseAeroclubTransportSearchConverterOut):
    """
    aeroclub /search/{request_id}/options/{option_id}/results response -> trip /search response
    """
    def convert_search_location(self, location_data: dict) -> RailLocation:
        city = location_data.get('city', {})
        return RailLocation(
            city=self.convert_location(city, ObjectType.city),
            country=self.convert_location(city['country'], ObjectType.country),
        )

    def convert_trip_location(self, trip_location_data: dict) -> RailLocation:
        city = trip_location_data.get('city', {})
        return RailLocation(
            city=self.convert_location(city, ObjectType.city),
            country=self.convert_location(city['country'], ObjectType.country),
            train_station=self.convert_location(trip_location_data, ObjectType.train_station),
        )

    def convert_carriage(self, carriage_data: dict) -> TrainCarriage:
        return TrainCarriage(
            min_price=carriage_data['min_price'],
            carriage_type=TrainCarriageType(carriage_data['type']).name,
            carriage_owner=carriage_data['carriage_owner'],
            is_travel_policy_compliant=carriage_data['is_travel_policy_compliant'],
            travel_policy_violations=self.convert_travel_policy_violations(carriage_data),
            place_count=carriage_data['seat_count'],
        )

    def convert_train(self, train_data: dict) -> Train:
        return Train(
            id=self.convert_id(train_data),
            departure=self.convert_trip_location(train_data['departure_from']),
            arrival=self.convert_trip_location(train_data['arrival_to']),
            departure_at=self.convert_date_time(train_data['departure_at']),
            departure_at_utc=self.convert_date_time(train_data['departure_at_utc']),
            arrival_at=self.convert_date_time(train_data['arrival_at']),
            arrival_at_utc=self.convert_date_time(train_data['arrival_at_utc']),
            train_name=train_data['train_name'],
            train_number=train_data['train_code'],
            ride_duration=self.convert_trip_duration(train_data['duration']),
            carriages=[
                self.convert_carriage(carriage_data)
                for carriage_data in train_data['carriages_info']
            ],
        )

    @staticmethod
    def convert_page(data: dict) -> int:
        return data['page_number'] + 1

    def convert(self, data: dict) -> RailSearchResult:
        return RailSearchResult(
            total=data['items_count'],
            page=self.convert_page(data),
            limit=data['items_per_page'],
            data=[self.convert_train(train) for train in data['data']],
        )


class AeroclubRailSearchInfoConverterOut(BaseAeroclubSearchInfoConverterOut):
    """
    aeroclub /search/{request_id} response -> trip /search/rail/{search_id}/info response
    """
    def convert_search_location(self, city_data: dict) -> RailLocation:
        return RailLocation(
            city=self.convert_location(city_data, ObjectType.city),
            country=self.convert_location(city_data['located_in'], ObjectType.country),
        )

    def convert(self, data: dict) -> RailSearchInfo:
        return RailSearchInfo(
            location_from=self.convert_search_location(city_data=data['data']['departure_city']),
            location_to=self.convert_search_location(data['data']['arrival_city']),
            departure_on=self.convert_date_time(data['data']['departure_on']).date(),
            status=self.convert_status(data['data']['status']),
        )


class AeroclubRailDetailConverterOut(AeroclubRailSearchConverterOut):
    # TODO: неподходящее название класса

    @staticmethod
    def convert_place(place_data: dict) -> TrainCarriagePlace:
        return TrainCarriagePlace(
            place_number=place_data['number'],
            compartment_gender=TrainCompartmentGender(place_data['compartment_type']).name,
        )

    @staticmethod
    def convert_services(services_data: list) -> list[str]:
        services = []
        for service in services_data or []:
            try:
                services.append(TrainCarriageClassOption(service).name)
            except ValueError:
                pass
        return services

    def convert_carriage(self, carriage_data: dict) -> TrainCarriageDetail:
        return TrainCarriageDetail(
            has_electronic_registration=carriage_data['is_electronic_registration_allowed'],
            min_price=carriage_data['min_price_for_class'],
            max_price=carriage_data['max_price_for_class'],
            carriage_type=TrainCarriageType(carriage_data['carriage_type']).name,
            carriage_owner=carriage_data['carriage_owner'],
            place_count=len(carriage_data['places']),
            is_travel_policy_compliant=carriage_data['is_travel_policy_compliant'],
            travel_policy_violations=self.convert_travel_policy_violations(carriage_data),
            carriage_number=int(carriage_data['carriage_num']),
            service_class_code=carriage_data['class']['code'],
            service_class_description=carriage_data['class']['name'][self.lang],
            services=self.convert_services(carriage_data['carriage_class_options']),
            places=[self.convert_place(place) for place in carriage_data['places']],
        )

    def convert(self, data: dict) -> RailDetailResponse:
        return RailDetailResponse(
            data=[
                self.convert_carriage(carriage_data)
                for carriage_data in data['data']['carriage_details']
            ],
            train=BaseTrain(
                departure=self.convert_trip_location(data['data']['departure_from']),
                arrival=self.convert_trip_location(data['data']['arrival_to']),
                departure_at=self.convert_date_time(data['data']['departure_at']),
                departure_at_utc=self.convert_date_time(data['data']['departure_at_utc']),
                arrival_at=self.convert_date_time(data['data']['arrival_at']),
                arrival_at_utc=self.convert_date_time(data['data']['arrival_at_utc']),
                train_name=data['data']['train_name'],
                train_number=data['data']['train_code'],
                ride_duration=self.convert_trip_duration(data['data']['duration']),
            ),
        )
