package ru.yandex.qe.dispenser.ws.base_resources.impl;

import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import javax.inject.Inject;

import com.google.common.collect.Sets;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import ru.yandex.qe.dispenser.api.v1.DiBotBigOrder;
import ru.yandex.qe.dispenser.api.v1.DiCampaign;
import ru.yandex.qe.dispenser.api.v1.DiResource;
import ru.yandex.qe.dispenser.api.v1.DiSegment;
import ru.yandex.qe.dispenser.api.v1.DiService;
import ru.yandex.qe.dispenser.domain.Campaign;
import ru.yandex.qe.dispenser.domain.Resource;
import ru.yandex.qe.dispenser.domain.Segment;
import ru.yandex.qe.dispenser.domain.Service;
import ru.yandex.qe.dispenser.domain.base_resources.BaseResource;
import ru.yandex.qe.dispenser.domain.base_resources.BaseResourceLinearRelationTerm;
import ru.yandex.qe.dispenser.domain.base_resources.BaseResourceMapping;
import ru.yandex.qe.dispenser.domain.base_resources.BaseResourceRelation;
import ru.yandex.qe.dispenser.domain.base_resources.BaseResourceType;
import ru.yandex.qe.dispenser.domain.bot.BigOrder;
import ru.yandex.qe.dispenser.domain.dao.base_resources.BaseResourceCache;
import ru.yandex.qe.dispenser.domain.dao.base_resources.BaseResourceMappingDao;
import ru.yandex.qe.dispenser.domain.dao.base_resources.BaseResourceTypeCache;
import ru.yandex.qe.dispenser.domain.dao.campaign.CampaignCache;
import ru.yandex.qe.dispenser.domain.dao.resource.ResourceReader;
import ru.yandex.qe.dispenser.domain.dao.segment.SegmentReader;
import ru.yandex.qe.dispenser.domain.dao.service.ServiceReader;
import ru.yandex.qe.dispenser.domain.hierarchy.Hierarchy;
import ru.yandex.qe.dispenser.domain.hierarchy.HierarchySupplier;
import ru.yandex.qe.dispenser.domain.i18n.LocalizableString;
import ru.yandex.qe.dispenser.domain.index.LongIndexBase;
import ru.yandex.qe.dispenser.domain.util.LocalizationUtils;
import ru.yandex.qe.dispenser.ws.base_resources.model.BaseResourceDto;
import ru.yandex.qe.dispenser.ws.base_resources.model.BaseResourceLinearRelationDto;
import ru.yandex.qe.dispenser.ws.base_resources.model.BaseResourceLinearRelationTermDto;
import ru.yandex.qe.dispenser.ws.base_resources.model.BaseResourceMappingDto;
import ru.yandex.qe.dispenser.ws.base_resources.model.BaseResourceMappingsPageDto;
import ru.yandex.qe.dispenser.ws.base_resources.model.BaseResourceMdsRelationDto;
import ru.yandex.qe.dispenser.ws.base_resources.model.BaseResourceMdsRelationTermDto;
import ru.yandex.qe.dispenser.ws.base_resources.model.BaseResourceRelationDto;
import ru.yandex.qe.dispenser.ws.base_resources.model.BaseResourceTypeDto;
import ru.yandex.qe.dispenser.ws.base_resources.model.ExpandBaseResourceMapping;
import ru.yandex.qe.dispenser.ws.base_resources.model.SingleBaseResourceMappingDto;
import ru.yandex.qe.dispenser.ws.bot.BigOrderManager;
import ru.yandex.qe.dispenser.ws.common.domain.errors.ErrorCollection;
import ru.yandex.qe.dispenser.ws.common.domain.errors.TypedError;
import ru.yandex.qe.dispenser.ws.common.domain.result.Result;

@Component
public class BaseResourceMappingsManager {

    private final MessageSource errorMessageSource;
    private final BaseResourceMappingDao baseResourceMappingDao;
    private final BaseResourceCache baseResourceCache;
    private final BaseResourceTypeCache baseResourceTypeCache;
    private final HierarchySupplier hierarchySupplier;
    private final CampaignCache campaignCache;
    private final BigOrderManager bigOrderManager;

    @Inject
    public BaseResourceMappingsManager(@Qualifier("errorMessageSource") MessageSource errorMessageSource,
                                       BaseResourceMappingDao baseResourceMappingDao,
                                       BaseResourceCache baseResourceCache,
                                       BaseResourceTypeCache baseResourceTypeCache,
                                       HierarchySupplier hierarchySupplier,
                                       CampaignCache campaignCache,
                                       BigOrderManager bigOrderManager) {
        this.errorMessageSource = errorMessageSource;
        this.baseResourceMappingDao = baseResourceMappingDao;
        this.baseResourceCache = baseResourceCache;
        this.baseResourceTypeCache = baseResourceTypeCache;
        this.hierarchySupplier = hierarchySupplier;
        this.campaignCache = campaignCache;
        this.bigOrderManager = bigOrderManager;
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public Result<BaseResourceMappingsPageDto, ErrorCollection<String, TypedError<String>>> getPage(
            Long from, Integer limit, Locale locale, Set<ExpandBaseResourceMapping> expand) {
        if (limit != null && (limit < 0 || limit > 100)) {
            return Result.failure(ErrorCollection.typedStringBuilder().addError("limit",
                    TypedError.badRequest(LocalizationUtils.resolveWithDefaultAsKey(errorMessageSource,
                            LocalizableString.of("invalid.limit.value"), locale))).build());
        }
        int actualLimit = limit == null ? 100 : limit;
        Set<ExpandBaseResourceMapping> actualExpand = expand != null ? expand : Set.of();
        List<BaseResourceMapping> baseResourceMappings = baseResourceMappingDao.getPage(from, actualLimit);
        BaseResourceMappingsPageDto result = preparePageDto(baseResourceMappings, actualExpand);
        return Result.success(result);
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public Result<SingleBaseResourceMappingDto, ErrorCollection<String, TypedError<String>>> getById(
            long id, Locale locale, Set<ExpandBaseResourceMapping> expand) {
        Optional<BaseResourceMapping> baseResourceMappingO = baseResourceMappingDao.getById(id);
        if (baseResourceMappingO.isEmpty()) {
            return Result.failure(ErrorCollection.typedStringBuilder().addError(
                    TypedError.notFound(LocalizationUtils.resolveWithDefaultAsKey(errorMessageSource,
                            LocalizableString.of("base.resource.mapping.not.found"), locale))).build());
        }
        Set<ExpandBaseResourceMapping> actualExpand = expand != null ? expand : Set.of();
        BaseResourceMapping baseResourceMapping = baseResourceMappingO.get();
        SingleBaseResourceMappingDto result = prepareSingleDto(baseResourceMapping, actualExpand);
        return Result.success(result);
    }

    private BaseResourceMappingsPageDto preparePageDto(List<BaseResourceMapping> baseResourceMappings,
                                                       Set<ExpandBaseResourceMapping> expand) {
        Set<Long> baseResourceIds = baseResourceMappings.stream().map(BaseResourceMapping::getBaseResourceId)
                .collect(Collectors.toSet());
        boolean expandBaseResources = expand.contains(ExpandBaseResourceMapping.BASE_RESOURCES);
        Set<BaseResource> baseResources = expandBaseResources
                ? baseResourceCache.getByIds(baseResourceIds) : Set.of();
        Set<Long> baseResourceTypeIds = baseResources.stream().map(BaseResource::getBaseResourceTypeId)
                .collect(Collectors.toSet());
        boolean expandTypes = expand.contains(ExpandBaseResourceMapping.BASE_RESOURCE_TYPES);
        Set<BaseResourceType> baseResourceTypes = expandTypes
                ? baseResourceTypeCache.getByIds(baseResourceTypeIds) : Set.of();
        Set<Long> campaignIds = baseResourceMappings.stream().map(BaseResourceMapping::getCampaignId)
                .collect(Collectors.toSet());
        Set<Long> bigOrderIds = baseResourceMappings.stream().map(BaseResourceMapping::getBigOrderId)
                .collect(Collectors.toSet());
        boolean expandCampaigns = expand.contains(ExpandBaseResourceMapping.CAMPAIGNS);
        boolean expandBigOrders = expand.contains(ExpandBaseResourceMapping.BIG_ORDERS);
        Set<Campaign> campaigns = expandCampaigns ? campaignCache.getByIds(campaignIds) : Set.of();
        Set<BigOrder> bigOrders = expandBigOrders ? bigOrderManager.getByIdsCached(bigOrderIds) : Set.of();
        Hierarchy hierarchy = hierarchySupplier.get();
        ServiceReader serviceReader = hierarchy.getServiceReader();
        SegmentReader segmentReader = hierarchy.getSegmentReader();
        ResourceReader resourceReader = hierarchy.getResourceReader();
        Set<Long> baseResourcesSegmentIds = baseResources.stream().flatMap(r -> r.getSegmentIds().stream())
                .collect(Collectors.toSet());
        Set<Long> relationsSegmentIds = baseResourceMappings.stream()
                .flatMap(m -> getRelationSegmentIds(m.getRelation()).stream()).collect(Collectors.toSet());
        Set<Long> segmentIds = Sets.union(baseResourcesSegmentIds, relationsSegmentIds);
        Set<Long> serviceIdsFromTypes = baseResourceTypes.stream().map(BaseResourceType::getServiceId)
                .collect(Collectors.toSet());
        Set<Long> serviceIdsFromMappings = baseResourceMappings.stream().map(BaseResourceMapping::getServiceId)
                .collect(Collectors.toSet());
        Set<Long> serviceIds = Sets.union(serviceIdsFromTypes, serviceIdsFromMappings);
        Set<Long> resourceIds = baseResourceMappings.stream()
                .flatMap(m -> getRelationResourceIds(m.getRelation()).stream()).collect(Collectors.toSet());
        List<BaseResourceMappingDto> baseResourceMappingDto = baseResourceMappings.stream()
                .map(r -> new BaseResourceMappingDto(r.getId(), r.getBaseResourceId(), r.getCampaignId(),
                        r.getBigOrderId(), r.getServiceId(), prepareRelationDto(r.getRelation())))
                .collect(Collectors.toList());
        boolean expandServices = expand.contains(ExpandBaseResourceMapping.PROVIDERS);
        boolean expandSegments = expand.contains(ExpandBaseResourceMapping.SEGMENTS);
        boolean expandResources = expand.contains(ExpandBaseResourceMapping.RESOURCES);
        Set<Service> services = expandServices ? serviceReader.readByIds(serviceIds) : Set.of();
        Set<Segment> segments = expandSegments ? segmentReader.readByIds(segmentIds) : Set.of();
        Set<Resource> resources = expandResources ? resourceReader.readByIds(resourceIds) : Set.of();
        Map<Long, BaseResourceDto> baseResourceById = baseResources.stream().collect(Collectors
                .toMap(BaseResource::getId, r -> new BaseResourceDto(r.getId(), r.getKey(), r.getName(),
                        r.getBaseResourceTypeId(), r.getSegmentIds())));
        Map<Long, DiService> servicesById = services.stream()
                .collect(Collectors.toMap(LongIndexBase::getId, Service::toView));
        Map<Long, DiSegment> segmentsById = segments.stream()
                .collect(Collectors.toMap(LongIndexBase::getId, Segment::toView));
        Map<Long, BaseResourceTypeDto> baseResourceTypesById = baseResourceTypes.stream()
                .collect(Collectors.toMap(BaseResourceType::getId, t -> new BaseResourceTypeDto(t.getId(), t.getKey(),
                        t.getName(), t.getServiceId(), t.getResourceType(), t.getResourceType().getBaseUnit())));
        Map<Long, DiResource> resourceById = resources.stream()
                .collect(Collectors.toMap(LongIndexBase::getId, Resource::toView));
        Map<Long, DiCampaign> campaignById = campaigns.stream()
                .collect(Collectors.toMap(LongIndexBase::getId, Campaign::toView));
        Map<Long, DiBotBigOrder> bigOrderById = bigOrders.stream().collect(Collectors.toMap(BigOrder::getId,
                o -> new DiBotBigOrder(o.getId(), o.getDate().format(DateTimeFormatter.ISO_LOCAL_DATE))));
        return new BaseResourceMappingsPageDto(baseResourceMappingDto, baseResourceById, baseResourceTypesById,
                servicesById, campaignById, bigOrderById, segmentsById, resourceById, baseResourceMappings.isEmpty()
                ? null : baseResourceMappings.get(baseResourceMappings.size() - 1).getId());
    }

    private SingleBaseResourceMappingDto prepareSingleDto(BaseResourceMapping baseResourceMapping,
                                                          Set<ExpandBaseResourceMapping> expand) {
        boolean expandBaseResources = expand.contains(ExpandBaseResourceMapping.BASE_RESOURCES);
        Optional<BaseResource> baseResourceO = expandBaseResources
                ? Optional.of(baseResourceCache.getById(baseResourceMapping.getBaseResourceId())
                        .orElseThrow(() -> new RuntimeException("Base resource "
                                + baseResourceMapping.getBaseResourceId() + " not found")))
                : Optional.empty();
        boolean expandTypes = expand.contains(ExpandBaseResourceMapping.BASE_RESOURCE_TYPES);
        Optional<BaseResourceType> baseResourceTypeO = expandTypes
                ? baseResourceO.map(baseResource -> baseResourceTypeCache.getById(baseResource.getBaseResourceTypeId())
                        .orElseThrow(() -> new RuntimeException("Base resource type "
                                + baseResource.getBaseResourceTypeId() + " not found")))
                : Optional.empty();
        boolean expandCampaigns = expand.contains(ExpandBaseResourceMapping.CAMPAIGNS);
        boolean expandBigOrders = expand.contains(ExpandBaseResourceMapping.BIG_ORDERS);
        Optional<Campaign> campaignO = expandCampaigns
                ? Optional.of(campaignCache.getById(baseResourceMapping.getCampaignId())
                        .orElseThrow(() -> new RuntimeException("Campaign "
                                + baseResourceMapping.getCampaignId() + " not found")))
                : Optional.empty();
        Optional<BigOrder> bigOrderO = expandBigOrders
                ? Optional.of(Objects.requireNonNull(bigOrderManager.getByIdCached(baseResourceMapping.getBigOrderId()),
                    "Big order " + baseResourceMapping.getBigOrderId() + " not found"))
                : Optional.empty();
        Hierarchy hierarchy = hierarchySupplier.get();
        ServiceReader serviceReader = hierarchy.getServiceReader();
        SegmentReader segmentReader = hierarchy.getSegmentReader();
        ResourceReader resourceReader = hierarchy.getResourceReader();
        boolean expandServices = expand.contains(ExpandBaseResourceMapping.PROVIDERS);
        boolean expandSegments = expand.contains(ExpandBaseResourceMapping.SEGMENTS);
        boolean expandResources = expand.contains(ExpandBaseResourceMapping.RESOURCES);
        Set<Long> baseResourcesSegmentIds = baseResourceO.map(BaseResource::getSegmentIds).orElse(Set.of());
        Set<Long> relationsSegmentIds = getRelationSegmentIds(baseResourceMapping.getRelation());
        Set<Long> segmentIds = Sets.union(baseResourcesSegmentIds, relationsSegmentIds);
        Set<Long> resourceIds = getRelationResourceIds(baseResourceMapping.getRelation());
        Set<Segment> segments = expandSegments ? segmentReader.readByIds(segmentIds) : Set.of();
        Set<Resource> resources = expandResources ? resourceReader.readByIds(resourceIds) : Set.of();
        Set<Long> serviceIdsFromType = baseResourceTypeO.map(BaseResourceType::getServiceId).stream()
                .collect(Collectors.toSet());
        Set<Long> serviceIdsFromMapping = Set.of(baseResourceMapping.getServiceId());
        Set<Long> serviceIds = Sets.union(serviceIdsFromType, serviceIdsFromMapping);
        Set<Service> services = expandServices ? serviceReader.readByIds(serviceIds) : Set.of();
        Map<Long, DiSegment> segmentsById = segments.stream()
                .collect(Collectors.toMap(LongIndexBase::getId, Segment::toView));
        BaseResourceDto baseResourceDto = baseResourceO.map(baseResource -> new BaseResourceDto(baseResource.getId(),
                baseResource.getKey(), baseResource.getName(), baseResource.getBaseResourceTypeId(),
                baseResource.getSegmentIds())).orElse(null);
        BaseResourceTypeDto baseResourceTypeDto = baseResourceTypeO.map(baseResourceType ->
                new BaseResourceTypeDto(baseResourceType.getId(), baseResourceType.getKey(),
                        baseResourceType.getName(), baseResourceType.getServiceId(),
                        baseResourceType.getResourceType(), baseResourceType.getResourceType().getBaseUnit()))
                .orElse(null);
        BaseResourceMappingDto baseResourceMappingDto = new BaseResourceMappingDto(baseResourceMapping.getId(),
                baseResourceMapping.getBaseResourceId(), baseResourceMapping.getCampaignId(),
                baseResourceMapping.getBigOrderId(), baseResourceMapping.getServiceId(),
                prepareRelationDto(baseResourceMapping.getRelation()));
        DiCampaign campaignDto = campaignO.map(Campaign::toView).orElse(null);
        DiBotBigOrder bigOrderDto = bigOrderO.map(o -> new DiBotBigOrder(o.getId(), o.getDate().format(DateTimeFormatter.ISO_LOCAL_DATE)))
                .orElse(null);
        Map<Long, DiService> servicesById = services.stream()
                .collect(Collectors.toMap(LongIndexBase::getId, Service::toView));
        Map<Long, DiResource> resourceById = resources.stream()
                .collect(Collectors.toMap(LongIndexBase::getId, Resource::toView));
        return new SingleBaseResourceMappingDto(baseResourceMappingDto, baseResourceDto, baseResourceTypeDto,
                servicesById, campaignDto, bigOrderDto, segmentsById, resourceById);
    }

    private BaseResourceRelationDto prepareRelationDto(BaseResourceRelation relation) {
        return new BaseResourceRelationDto(relation.getLinear().map(linear -> new BaseResourceLinearRelationDto(linear
                .getTerms().stream().map(term -> new BaseResourceLinearRelationTermDto(term.getNumerator(),
                        term.getDenominator(), term.getResourceId(), term.getSegmentIds())).collect(Collectors.toList()),
                linear.getConstantTermNumerator(), linear.getConstantTermDenominator())).orElse(null),
                relation.getMds().map(mds -> new BaseResourceMdsRelationDto(mds.getTerms().stream()
                        .map(term -> new BaseResourceMdsRelationTermDto(term.getResourceId(), term.getSegmentIds(),
                                term.getLocation(), term.getStorageType(), term.getNumerator(), term.getDenominator(),
                                term.getAbcServiceId().orElse(null)))
                        .collect(Collectors.toList()))).orElse(null));
    }

    private Set<Long> getRelationSegmentIds(BaseResourceRelation relation) {
        return relation.getLinear().map(linear -> linear.getTerms().stream()
                .flatMap(term -> term.getSegmentIds().stream()).collect(Collectors.toSet())).orElse(Set.of());
    }

    private Set<Long> getRelationResourceIds(BaseResourceRelation relation) {
        return relation.getLinear().map(linear -> linear.getTerms().stream()
                .map(BaseResourceLinearRelationTerm::getResourceId).collect(Collectors.toSet())).orElse(Set.of());
    }

}
