package ru.yandex.direct.api.v5.entity.dictionaries;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TimeZone;

import com.yandex.direct.api.v5.ads.AdCategoryEnum;
import com.yandex.direct.api.v5.dictionaries.AdCategoriesItem;
import com.yandex.direct.api.v5.dictionaries.AudienceCriteriaTypesItem;
import com.yandex.direct.api.v5.dictionaries.AudienceDemographicProfilesItem;
import com.yandex.direct.api.v5.dictionaries.AudienceInterestsItem;
import com.yandex.direct.api.v5.dictionaries.ConstantsItem;
import com.yandex.direct.api.v5.dictionaries.CurrenciesItem;
import com.yandex.direct.api.v5.dictionaries.DictionaryNameEnum;
import com.yandex.direct.api.v5.dictionaries.FilterSchemasItem;
import com.yandex.direct.api.v5.dictionaries.GeoRegionsItem;
import com.yandex.direct.api.v5.dictionaries.GetRequest;
import com.yandex.direct.api.v5.dictionaries.GetResponse;
import com.yandex.direct.api.v5.dictionaries.InterestsItem;
import com.yandex.direct.api.v5.dictionaries.MetroStationsItem;
import com.yandex.direct.api.v5.dictionaries.OperationSystemVersionsItem;
import com.yandex.direct.api.v5.dictionaries.SupplySidePlatformsItem;
import com.yandex.direct.api.v5.dictionaries.TimeZonesItem;
import one.util.streamex.EntryStream;
import one.util.streamex.StreamEx;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import ru.yandex.direct.api.v5.context.ApiContextHolder;
import ru.yandex.direct.api.v5.entity.dictionaries.categories.CategoryTranslationHolder;
import ru.yandex.direct.api.v5.entity.dictionaries.converter.FilterSchemaConverterKt;
import ru.yandex.direct.api.v5.entity.dictionaries.crypta.BlockElementType;
import ru.yandex.direct.api.v5.entity.dictionaries.exception.OldVersionOfAndroidApplicationException;
import ru.yandex.direct.api.v5.security.ApiAuthenticationSource;
import ru.yandex.direct.api.v5.service.accelinfo.AccelInfoHeaderSetter;
import ru.yandex.direct.api.v5.units.ApiUnitsService;
import ru.yandex.direct.api.v5.validation.DefectType;
import ru.yandex.direct.common.TranslationService;
import ru.yandex.direct.common.db.PpcPropertiesSupport;
import ru.yandex.direct.common.db.PpcProperty;
import ru.yandex.direct.core.entity.banner.model.BannerFlags;
import ru.yandex.direct.core.entity.banner.type.title.BannerConstantsService;
import ru.yandex.direct.core.entity.crypta.AudienceType;
import ru.yandex.direct.core.entity.crypta.repository.CryptaSegmentRepository;
import ru.yandex.direct.core.entity.feature.service.FeatureService;
import ru.yandex.direct.core.entity.mobilecontent.model.OsType;
import ru.yandex.direct.core.entity.performancefilter.service.PerformanceFilterStorage;
import ru.yandex.direct.core.entity.retargeting.model.CryptaInterestType;
import ru.yandex.direct.core.entity.retargeting.model.Goal;
import ru.yandex.direct.core.entity.retargeting.model.TargetingCategory;
import ru.yandex.direct.core.entity.retargeting.repository.TargetingCategoriesRepository;
import ru.yandex.direct.core.entity.sspplatform.repository.SspPlatformsRepository;
import ru.yandex.direct.core.entity.timetarget.model.GeoTimezone;
import ru.yandex.direct.core.entity.timetarget.model.GroupType;
import ru.yandex.direct.core.entity.timetarget.repository.GeoTimezoneRepository;
import ru.yandex.direct.core.units.OperationSummary;
import ru.yandex.direct.currency.Currencies;
import ru.yandex.direct.currency.Currency;
import ru.yandex.direct.currency.CurrencyCode;
import ru.yandex.direct.currency.Rate;
import ru.yandex.direct.currency.RateUtils;
import ru.yandex.direct.dbutil.model.ClientId;
import ru.yandex.direct.regions.GeoTreeFactory;
import ru.yandex.direct.regions.Metro;
import ru.yandex.direct.regions.Region;
import ru.yandex.direct.validation.builder.ItemValidationBuilder;
import ru.yandex.direct.validation.builder.When;
import ru.yandex.direct.validation.result.ValidationResult;
import ru.yandex.misc.lang.StringUtils;

import static com.yandex.direct.api.v5.dictionaries.CanSelectEnum.ALL;
import static com.yandex.direct.api.v5.dictionaries.CanSelectEnum.EXCEPT_ALL;
import static com.yandex.direct.api.v5.dictionaries.DictionaryNameEnum.AD_CATEGORIES;
import static com.yandex.direct.api.v5.dictionaries.DictionaryNameEnum.AUDIENCE_CRITERIA_TYPES;
import static com.yandex.direct.api.v5.dictionaries.DictionaryNameEnum.AUDIENCE_DEMOGRAPHIC_PROFILES;
import static com.yandex.direct.api.v5.dictionaries.DictionaryNameEnum.AUDIENCE_INTERESTS;
import static com.yandex.direct.api.v5.dictionaries.DictionaryNameEnum.CONSTANTS;
import static com.yandex.direct.api.v5.dictionaries.DictionaryNameEnum.CURRENCIES;
import static com.yandex.direct.api.v5.dictionaries.DictionaryNameEnum.FILTER_SCHEMAS;
import static com.yandex.direct.api.v5.dictionaries.DictionaryNameEnum.GEO_REGIONS;
import static com.yandex.direct.api.v5.dictionaries.DictionaryNameEnum.INTERESTS;
import static com.yandex.direct.api.v5.dictionaries.DictionaryNameEnum.METRO_STATIONS;
import static com.yandex.direct.api.v5.dictionaries.DictionaryNameEnum.OPERATION_SYSTEM_VERSIONS;
import static com.yandex.direct.api.v5.dictionaries.DictionaryNameEnum.SUPPLY_SIDE_PLATFORMS;
import static com.yandex.direct.api.v5.dictionaries.DictionaryNameEnum.TIME_ZONES;
import static com.yandex.direct.api.v5.general.YesNoEnum.NO;
import static com.yandex.direct.api.v5.general.YesNoEnum.YES;
import static java.lang.Math.abs;
import static java.util.Collections.emptyList;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
import static ru.yandex.direct.api.v5.common.ConverterUtils.convertToMicros;
import static ru.yandex.direct.api.v5.entity.ads.converter.CategoriesConverter.CATEGORIES;
import static ru.yandex.direct.api.v5.entity.dictionaries.converter.FilterSchemaConverterKt.getEmptyFilterSchemasItem;
import static ru.yandex.direct.api.v5.entity.dictionaries.validation.DictionariesConstraints.mustUseNewVersionOfAndroidApplication;
import static ru.yandex.direct.api.v5.entity.retargetinglists.converter.GoalInterestsTypeConverter.getExternalId;
import static ru.yandex.direct.api.v5.entity.retargetinglists.converter.GoalInterestsTypeConverter.getExternalType;
import static ru.yandex.direct.common.db.PpcPropertyNames.GBP_CLIENT_UID;
import static ru.yandex.direct.common.db.PpcPropertyNames.GBP_CLIENT_UID_ALLOWED_VALUE;
import static ru.yandex.direct.core.entity.banner.model.BannerFlags.MED_EQUIPMENT;
import static ru.yandex.direct.core.entity.banner.model.BannerFlags.PHARMACY;
import static ru.yandex.direct.core.entity.banner.service.validation.BannerTextConstants.MAX_NUMBER_OF_NARROW_CHARACTERS;
import static ru.yandex.direct.core.entity.banner.type.body.BannerWithBodyConstants.MAX_LENGTH_BODY;
import static ru.yandex.direct.core.entity.banner.type.body.BannerWithBodyConstants.MAX_LENGTH_BODY_WORD;
import static ru.yandex.direct.core.entity.banner.type.body.BannerWithBodyConstants.MAX_LENGTH_MOBILE_BODY;
import static ru.yandex.direct.core.entity.banner.type.title.BannerConstantsService.MAX_LENGTH_TITLE_WORD;
import static ru.yandex.direct.core.entity.banner.type.titleextension.BannerWithTitleExtensionConstants.MAX_LENGTH_TITLE_EXTENSION;
import static ru.yandex.direct.core.entity.campaign.service.validation.CampaignWithStrategyValidationUtils.getAutobudgetPayForConversionAvgCpaWarning;
import static ru.yandex.direct.core.entity.sitelink.service.validation.SitelinkConstants.MAX_SITELINK_DESC_LENGTH;
import static ru.yandex.direct.core.entity.sitelink.service.validation.SitelinkConstants.MAX_SITELINK_TITLE_LENGTH;
import static ru.yandex.direct.core.entity.sitelink.service.validation.SitelinkConstants.SITELINKS_MAX_LENGTH;
import static ru.yandex.direct.core.entity.sitelink.service.validation.SitelinkSetValidationService.MAX_SITELINKS_PER_SET;
import static ru.yandex.direct.core.entity.timetarget.model.GroupType.CIS;
import static ru.yandex.direct.core.entity.timetarget.model.GroupType.RUSSIA;
import static ru.yandex.direct.core.entity.timetarget.model.GroupType.WORLD;
import static ru.yandex.direct.core.validation.constraints.MobileContentConstraints.OS_VERSIONS;
import static ru.yandex.direct.utils.DateTimeUtils.MOSCOW_TIMEZONE;

@Service
public class DictionariesService {
    /**
     * Внимание, для элемента "Interests" сервис использует нестандартную схему i18n:
     * <br>ключи из keyset-а
     * <a href="https://tanker.yandex-team.ru/?project=direct-java&branch=master&keyset=TargetingCategories">profile</a>
     * необходимо скачать в формате tjson отдельно по каждому языку в файлы, на которые натравить скрипт:
     * <br>{@code python -c "import json; data = json.load(open('$FilePath$')); language =
     * data['export_info']['request']['languages'][0]; translations = {'dictionary': {key: {'is_plural':
     * value['info']['is_plural'], 'form': value['translations'][language]['form']} for key, value in
     * data['keysets']['TargetingCategories']['keys'].iteritems() if value['translations'][language]['form']}}; print
     * json.dumps(translations, sort_keys=True, ensure_ascii=False).encode('utf8')"}</br>
     * <br>где {@code $FilePath$} - полный путь к файлу</br>
     * <br>Выхлопы скрипта сложить в соответствующие файлики в проекте:</br>
     * <li/>/api5/src/main/resources/locale/ru/yandex/direct/api/v5/entity/dictionaries/targetingcategories
     * /CategoriesTranslations.ru.json
     * <li/>/api5/src/main/resources/locale/ru/yandex/direct/api/v5/entity/dictionaries/targetingcategories
     * /CategoriesTranslations.en.json
     * <li/>/api5/src/main/resources/locale/ru/yandex/direct/api/v5/entity/dictionaries/targetingcategories
     * /CategoriesTranslations.tr.json
     */
    private static final String TARGETING_CATEGORIES_TRANSLATIONS_BUNDLE_NAME
            = "ru.yandex.direct.api.v5.entity.dictionaries.targetingcategories.CategoriesTranslations";

    private static final String CRYPTA_GOAL_TRANSLATIONS_BUNDLE_NAME
            = "ru.yandex.direct.core.entity.crypta.CryptaGoalTranslations";
    private static final int MOSCOW_OFFSET = TimeZone.getTimeZone(MOSCOW_TIMEZONE).getRawOffset() / 1000;

    private static final Duration ONE_MINUTE = Duration.ofMinutes(1);

    private final ApiUnitsService apiUnitsService;
    private final AccelInfoHeaderSetter accelInfoHeaderSetter;
    private final TranslationService translationService;
    private final GeoTreeFactory geoTreeFactory;
    private final GeoTimezoneRepository geoTimezoneRepository;
    private final SspPlatformsRepository sspPlatformsRepository;
    private final TargetingCategoriesRepository targetingCategoriesRepository;
    private final CryptaSegmentRepository cryptaSegmentRepository;
    private final ApiContextHolder apiContextHolder;
    private final ApiAuthenticationSource apiAuthenticationSource;
    private final FeatureService featureService;
    private final BannerConstantsService bannerConstantsService;
    private final PerformanceFilterStorage performanceFilterStorage;

    private final PpcProperty<Set<Long>> gbpClientUidProperty;

    @Autowired
    public DictionariesService(
            ApiUnitsService apiUnitsService,
            AccelInfoHeaderSetter accelInfoHeaderSetter,
            TranslationService translationService,
            GeoTreeFactory geoTreeFactory,
            GeoTimezoneRepository geoTimezoneRepository,
            SspPlatformsRepository sspPlatformsRepository,
            TargetingCategoriesRepository targetingCategoriesRepository,
            CryptaSegmentRepository cryptaSegmentRepository,
            PpcPropertiesSupport ppcPropertiesSupport,
            ApiContextHolder apiContextHolder, ApiAuthenticationSource apiAuthenticationSource,
            FeatureService featureService, BannerConstantsService bannerConstantsService,
            PerformanceFilterStorage performanceFilterStorage) {
        this.apiUnitsService = apiUnitsService;
        this.accelInfoHeaderSetter = accelInfoHeaderSetter;
        this.translationService = translationService;
        this.geoTreeFactory = geoTreeFactory;
        this.geoTimezoneRepository = geoTimezoneRepository;
        this.sspPlatformsRepository = sspPlatformsRepository;
        this.targetingCategoriesRepository = targetingCategoriesRepository;
        this.cryptaSegmentRepository = cryptaSegmentRepository;
        this.apiContextHolder = apiContextHolder;
        this.gbpClientUidProperty = ppcPropertiesSupport.get(GBP_CLIENT_UID, ONE_MINUTE);
        this.apiAuthenticationSource = apiAuthenticationSource;
        this.featureService = featureService;
        this.bannerConstantsService = bannerConstantsService;
        this.performanceFilterStorage = performanceFilterStorage;
    }

    private ValidationResult<GetRequest, DefectType> validateRequest(GetRequest getRequest) {
        boolean isAndroidApp = apiContextHolder.get().getPreAuthentication().isAndroidMobileApplication();

        ItemValidationBuilder<GetRequest, DefectType> vb = ItemValidationBuilder.of(getRequest);

        vb.item(getRequest.getVersion(), "Version")
                .check(mustUseNewVersionOfAndroidApplication(),
                        When.isTrue(isAndroidApp));

        return vb.getResult();
    }

    public GetResponse get(GetRequest getRequest) {
        GetResponse getResponse = new GetResponse();

        ValidationResult<GetRequest, DefectType> vr = validateRequest(getRequest);

        if (vr.hasAnyErrors()) {
            throw new OldVersionOfAndroidApplicationException();
        }

        List<DictionaryNameEnum> dictionaryNames = getRequest.getDictionaryNames();

        ClientId clientId = apiAuthenticationSource.getChiefSubclient().getClientId();

        if (dictionaryNames.contains(CURRENCIES)) {
            Set<String> enabledFeatures = featureService.getEnabledForClientId(clientId);
            List<CurrenciesItem> currenciesItems = getCurrenciesItems(enabledFeatures);

            getResponse.setCurrencies(currenciesItems);
        }

        if (dictionaryNames.contains(METRO_STATIONS)) {
            getResponse.setMetroStations(getMetroStationsItems());
        }

        if (dictionaryNames.contains(GEO_REGIONS)) {
            getResponse.setGeoRegions(getGeoRegionsItems());
        }

        if (dictionaryNames.contains(TIME_ZONES)) {
            getResponse.setTimeZones(getTimeZonesItems());
        }

        if (dictionaryNames.contains(CONSTANTS)) {
            getResponse.setConstants(getConstantsItems(clientId));
        }

        if (dictionaryNames.contains(AD_CATEGORIES)) {
            getResponse.setAdCategories(getAdCategoriesItems());
        }

        if (dictionaryNames.contains(OPERATION_SYSTEM_VERSIONS)) {
            getResponse.setOperationSystemVersions(getOperationSystemVersionsItems());
        }

        if (dictionaryNames.contains(SUPPLY_SIDE_PLATFORMS)) {
            getResponse.setSupplySidePlatforms(getSupplySidePlatformsItems());
        }

        if (dictionaryNames.contains(INTERESTS)) {
            getResponse.setInterests(getInterestsItems());
        }

        if (dictionaryNames.contains(AUDIENCE_CRITERIA_TYPES)) {
            getResponse.setAudienceCriteriaTypes(getAudienceCriteriaTypesItems());
        }

        if (dictionaryNames.contains(AUDIENCE_DEMOGRAPHIC_PROFILES)) {
            getResponse.setAudienceDemographicProfiles(getAudienceDemographicProfilesItems());
        }

        if (dictionaryNames.contains(AUDIENCE_INTERESTS)) {
            getResponse.setAudienceInterests(getAudienceInterestsItems());
        }

        if (dictionaryNames.contains(FILTER_SCHEMAS)) {
            getResponse.setFilterSchemas(getFilterSchemasItems());
        }

        apiUnitsService.withdraw(OperationSummary.successful(1));

        accelInfoHeaderSetter.setAccelInfoHeaderToHttpResponse();

        return getResponse;
    }

    private List<AudienceInterestsItem> getAudienceInterestsItems() {
        List<AudienceInterestsItem> items = new ArrayList<>();
        for (Goal goal : cryptaSegmentRepository.getInterests().values()) {
            for (CryptaInterestType type : CryptaInterestType.values()) {
                AudienceInterestsItem item = new AudienceInterestsItem()
                        .withInterestKey(goal.getId())
                        .withId(getExternalId(goal.getId(), type))
                        .withParentId(getExternalId(goal.getParentId(), type))
                        .withInterestType(getExternalType(type))
                        .withName(translationService.translate(CRYPTA_GOAL_TRANSLATIONS_BUNDLE_NAME,
                                goal.getTankerNameKey()))
                        .withDescription(translationService.translate(CRYPTA_GOAL_TRANSLATIONS_BUNDLE_NAME,
                                goal.getTankerDescriptionKey()));
                items.add(item);
            }
        }

        return items;
    }

    private List<FilterSchemasItem> getFilterSchemasItems() {
        return StreamEx.of(performanceFilterStorage.getAllSchemas())
                .map(FilterSchemaConverterKt::toFilterSchemasItem)
                .append(getEmptyFilterSchemasItem())
                .sortedBy(FilterSchemasItem::getName)
                .toList();
    }

    private List<AudienceDemographicProfilesItem> getAudienceDemographicProfilesItems() {
        return cryptaSegmentRepository.getSocialDemo().values().stream()
                .map(goal -> new AudienceDemographicProfilesItem()
                        .withId(goal.getId())
                        .withType(AudienceType.fromTypedValue(goal.getParentId()).name())
                        .withName(translationService.translate(CRYPTA_GOAL_TRANSLATIONS_BUNDLE_NAME,
                                goal.getTankerNameKey()))
                        .withDescription(translationService.translate(CRYPTA_GOAL_TRANSLATIONS_BUNDLE_NAME,
                                goal.getTankerDescriptionKey()))
                ).collect(toList());
    }

    private List<AudienceCriteriaTypesItem> getAudienceCriteriaTypesItems() {
        List<AudienceCriteriaTypesItem> items = new ArrayList<>();
        for (Goal goal : cryptaSegmentRepository.getSocialDemoTypes().values()) {
            AudienceCriteriaTypesItem item = new AudienceCriteriaTypesItem()
                    .withType(AudienceType.fromTypedValue(goal.getId()).name())
                    .withBlockElement(BlockElementType.fromTypedValue(goal.getType()).name())
                    .withName(translationService.translate(CRYPTA_GOAL_TRANSLATIONS_BUNDLE_NAME,
                            goal.getTankerNameKey()))
                    .withDescription(translationService.translate(CRYPTA_GOAL_TRANSLATIONS_BUNDLE_NAME,
                            goal.getTankerDescriptionKey()))
                    .withCanSelect(AudienceType.fromTypedValue(goal.getId()).isAllValuesAllowed() ? ALL : EXCEPT_ALL);
            items.add(item);
        }
        return items;
    }

    private List<InterestsItem> getInterestsItems() {
        List<InterestsItem> interests = new ArrayList<>();
        for (TargetingCategory targetingCategory : targetingCategoriesRepository.getAll()) {
            InterestsItem item = new InterestsItem()
                    .withInterestId(targetingCategory.getTargetingCategoryId())
                    .withParentId(targetingCategory.getParentId())
                    .withName(translationService.translate(TARGETING_CATEGORIES_TRANSLATIONS_BUNDLE_NAME,
                            targetingCategory.getOriginalName()))
                    .withIsTargetable(targetingCategory.isAvailable() ? YES : NO);

            interests.add(item);
        }
        return interests;
    }

    private List<SupplySidePlatformsItem> getSupplySidePlatformsItems() {
        List<SupplySidePlatformsItem> platforms = new ArrayList<>();
        for (String platform : sspPlatformsRepository.getAllSspPlatforms()) {
            SupplySidePlatformsItem item = new SupplySidePlatformsItem()
                    .withTitle(platform);

            platforms.add(item);
        }
        return platforms;
    }

    private List<OperationSystemVersionsItem> getOperationSystemVersionsItems() {
        List<OperationSystemVersionsItem> operationSystemVersions = new ArrayList<>();
        for (Map.Entry<OsType, Set<String>> operationSystemEntry : OS_VERSIONS.entrySet()) {
            for (String version : operationSystemEntry.getValue()) {
                @SuppressWarnings("ConstantConditions")
                OperationSystemVersionsItem item = new OperationSystemVersionsItem()
                        .withOsName(OsType.toSource(operationSystemEntry.getKey()).toString())
                        .withOsVersion(version);

                operationSystemVersions.add(item);
            }
        }
        return operationSystemVersions;
    }

    private List<ConstantsItem> getConstantsItems(ClientId clientId) {
        int maxTitleLength = bannerConstantsService.getMaxLengthTitle(clientId);

        List<ConstantsItem> constants = new ArrayList<>();
        addConstant(constants, "MaximumAdTextLength", MAX_LENGTH_BODY);
        addConstant(constants, "MaximumAdTextWordLength", MAX_LENGTH_BODY_WORD);
        addConstant(constants, "MaximumAdTitleLength", maxTitleLength);
        addConstant(constants, "MaximumAdTitleWordLength", MAX_LENGTH_TITLE_WORD);
        addConstant(constants, "MaximumDynamicTextAdTextLength", MAX_LENGTH_BODY);
        addConstant(constants, "MaximumMobileAppAdTextLength", MAX_LENGTH_MOBILE_BODY);
        addConstant(constants, "MaximumMobileAppAdTitleLength", maxTitleLength);
        addConstant(constants, "MaximumNumberOfNarrowCharacters", MAX_NUMBER_OF_NARROW_CHARACTERS);
        addConstant(constants, "MaximumSitelinkDescriptionLength", MAX_SITELINK_DESC_LENGTH);
        addConstant(constants, "MaximumSitelinkTextLength", MAX_SITELINK_TITLE_LENGTH);
        addConstant(constants, "MaximumSitelinksLength", SITELINKS_MAX_LENGTH);
        addConstant(constants, "MaximumSitelinksNumber", MAX_SITELINKS_PER_SET);
        addConstant(constants, "MaximumTextAdTextLength", MAX_LENGTH_BODY);
        addConstant(constants, "MaximumTextAdTitle2Length", MAX_LENGTH_TITLE_EXTENSION);
        addConstant(constants, "MaximumTextAdTitleLength", maxTitleLength);
        return constants;
    }

    private List<AdCategoriesItem> getAdCategoriesItems() {
        List<AdCategoriesItem> categories = new ArrayList<>();
        for (Map.Entry<String, AdCategoryEnum> categoryEntry : CATEGORIES.entrySet()) {

            if (categoryEntry.getKey().equals(BannerFlags.MED_SERVICES.getKey())
                    || categoryEntry.getKey().equals(MED_EQUIPMENT.getKey())
                    || categoryEntry.getKey().equals(PHARMACY.getKey())) {
                continue;
            }

            AdCategoriesItem item = new AdCategoriesItem()
                    .withAdCategory(categoryEntry.getValue().toString())
                    .withDescription(translationService.translate(
                            CategoryTranslationHolder.ofCategory(categoryEntry.getKey()).shortForm()))
                    .withMessage(translationService.translate(
                            CategoryTranslationHolder.ofCategory(categoryEntry.getKey()).longForm()));

            categories.add(item);
        }
        return categories;
    }

    private List<TimeZonesItem> getTimeZonesItems() {

        long currentTime = new Date().getTime();
        List<TimeZonesItem> timeZones = new ArrayList<>();
        for (GeoTimezone geoTimezone : geoTimezoneRepository.getGeoTimezonesByTimezoneIds(emptyList())) {

            TimeZone timeZone = TimeZone.getTimeZone(geoTimezone.getTimezone());
            int gmtOffSet = timeZone.getOffset(currentTime) / 1000;
            String timeZoneName = getTimeZoneName(geoTimezone)
                    + getTimezoneOffset(timeZone, gmtOffSet, geoTimezone.getGroupType());

            TimeZonesItem item = new TimeZonesItem()
                    .withTimeZone(timeZone.getID())
                    .withTimeZoneName(timeZoneName)
                    .withUtcOffset(gmtOffSet);
            timeZones.add(item);
        }
        return timeZones.stream()
                .sorted(comparing(TimeZonesItem::getUtcOffset))
                .collect(toList());
    }

    private String getTimezoneOffset(TimeZone timeZone, int gmtOffSet, GroupType groupType) {
        if (groupType == RUSSIA) {
            if (MOSCOW_TIMEZONE.equals(timeZone.getID())) {
                return "";
            } else {
                return String.format(" (MSK %s)", getOffset(gmtOffSet - MOSCOW_OFFSET));
            }
        } else if (groupType == CIS) {
            return String.format(" (MSK %s, GMT %s)", getOffset(gmtOffSet - MOSCOW_OFFSET), getOffset(gmtOffSet));
        } else if (groupType == WORLD) {
            return String.format(" (GMT %s)", getOffset(gmtOffSet));
        } else {
            throw new IllegalArgumentException("Unsupported type: " + groupType);
        }
    }

    private String getOffset(int seconds) {
        int hours = seconds / 3600;
        int minutes = (seconds - hours * 3600) / 60;
        return String.format("%s%02d:%02d", (seconds >= 0 ? '+' : '-'), abs(hours), abs(minutes));
    }

    private List<GeoRegionsItem> getGeoRegionsItems() {
        List<GeoRegionsItem> geoRegions = new ArrayList<>();
        for (Region region : geoTreeFactory.getApiGeoTree().getRegions().values()) {
            GeoRegionsItem item = new GeoRegionsItem()
                    .withParentId(region.getId() == 0 ? null : region.getParent().getId()) // для Мира ParentId = Null
                    .withGeoRegionId(region.getId())
                    .withGeoRegionType(region.getTypeName())
                    .withGeoRegionName(getRegionName(region));
            geoRegions.add(item);
        }
        return geoRegions;
    }

    private List<MetroStationsItem> getMetroStationsItems() {
        List<MetroStationsItem> metroStations = new ArrayList<>();
        for (Metro metro : geoTreeFactory.getApiGeoTree().getMetroMap().values()) {
            MetroStationsItem item = new MetroStationsItem()
                    .withMetroStationId(metro.getId())
                    .withMetroStationName(metro.getName())
                    .withGeoRegionId(metro.getParent().getId());
            metroStations.add(item);
        }
        return metroStations;
    }

    private List<CurrenciesItem> getCurrenciesItems(Set<String> enabledFeatures) {
        List<CurrenciesItem> currencies = new ArrayList<>();


        boolean isAndroidApp = apiContextHolder.get().getPreAuthentication().isAndroidMobileApplication();

        boolean allowGbpCurrency = Objects.equals(gbpClientUidProperty.get(), GBP_CLIENT_UID_ALLOWED_VALUE);

        List<Currency> currencyList = EntryStream.of(Currencies.getCurrencies())
                .values()
                .filter(key -> !(key.getCode() == CurrencyCode.YND_FIXED) &&
                        (allowGbpCurrency || key.getCode() != CurrencyCode.GBP))
                .toList();
        String lang = translationService.getLocale().getLanguage();
        for (Currency currency : currencyList) {
            CurrenciesItem item = new CurrenciesItem();
            currencies.add(item);
            CurrencyCode currencyCode = currency.getCode();
            item.setCurrency(currencyCode.name());
            Rate rate = RateUtils.get(currencyCode);

            String fullName = translationService.translate(currencyCode.getTranslation().longForm());

            // Для России и Украины названия валют в перле возвращались с маленькой буквы
            if ("ru".equals(lang) || "uk".equals(lang)) {
                fullName = StringUtils.uncapitalize(fullName);
            }

            addProperty(item, "BidIncrement", convertToMicros(currency.getAuctionStep()).toString());
            addProperty(item, "FullName", fullName);

            addProperty(item, "MaximumBid", convertToMicros(currency.getMaxPrice()).toString());
            addProperty(item, "MinimumAverageCPA",
                    convertToMicros(currency.getMinAutobudgetAvgCpa()).toString());
            addProperty(item, "MinimumPayForConversionCPA",
                    convertToMicros(currency.getMinAutobudgetAvgCpa()).toString());
            addProperty(item, "MaximumPayForConversionCPA",
                    convertToMicros(getAutobudgetPayForConversionAvgCpaWarning(enabledFeatures, currency)).toString());
            addProperty(item, "MinimumAverageCPV",
                    convertToMicros(currency.getMinAvgCpv()).toString());
            addProperty(item, "MaximumAverageCPV",
                    convertToMicros(currency.getMaxAvgCpv()).toString());
            addProperty(item, "MinimumAverageCPC",
                    convertToMicros(currency.getMinAutobudgetAvgPrice()).toString());
            addProperty(item, "MinimumCPM",
                    convertToMicros(currency.getMinCpmPrice()).toString());
            addProperty(item, "MaximumCPM",
                    convertToMicros(currency.getMaxCpmPrice()).toString());
            addProperty(item, "MinimumBid", convertToMicros(currency.getMinPrice()).toString());
            addProperty(item, "MinimumDailyBudget", convertToMicros(currency.getMinDayBudget()).toString());
            addProperty(item, "MinimumPayment", convertToMicros(currency.getMinPay()).toString());
            addProperty(item, "MinimumAccountDailyBudget",
                    convertToMicros(currency.getMinWalletDayBudget()).toString());
            addProperty(item, "MinimumTransferAmount",
                    convertToMicros(currency.getMinTransferMoney()).toString());
            addProperty(item, "MinimumWeeklySpendLimit",
                    convertToMicros(currency.getMinAutobudget()).toString());
            addProperty(item, "MinDailyBudgetForPeriod",
                    convertToMicros(currency.getMinDailyBudgetForPeriod()).toString());
            addProperty(item, "MaxDailyBudgetForPeriod",
                    convertToMicros(currency.getMaxDailyBudgetForPeriod()).toString());
            addProperty(item, "MaxAutobudget", convertToMicros(currency.getMaxAutobudget()).toString());
            addProperty(item, "AutobudgetAvgCpaWarning", convertToMicros(currency.getAutobudgetAvgCpaWarning()).toString());
            addProperty(item, "MinCpcCpaPerformance", convertToMicros(currency.getMinCpcCpaPerformance()).toString());

        }
        return currencies;
    }

    private void addConstant(List<ConstantsItem> constants, String name, int value) {
        constants.add(new ConstantsItem().withName(name).withValue(String.valueOf(value)));
    }

    private void addProperty(CurrenciesItem currenciesItem, String name, String value) {
        currenciesItem.getProperties().add(new ConstantsItem().withName(name).withValue(value));
    }

    private String getTimeZoneName(GeoTimezone geoTimezone) {
        switch (translationService.getLocale().getLanguage()) {
            case "ru":
                return geoTimezone.getNameRu();
            case "en":
                return geoTimezone.getNameEn();
            case "uk":
                return geoTimezone.getNameUa();
            case "tr":
                return geoTimezone.getNameTr();
            default:
                return geoTimezone.getNameEn();
        }
    }

    private String getRegionName(Region region) {
        switch (translationService.getLocale().getLanguage()) {
            case "ru":
                return region.getNameRu();
            case "en":
                return region.getNameEn();
            case "uk":
                return region.getNameUa();
            case "tr":
                return region.getNameTr();
            default:
                return region.getNameEn();
        }
    }
}
