package ru.yandex.qe.dispenser.ws.quota.request.workflow;

import java.math.BigDecimal;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.ParametersAreNonnullByDefault;

import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import ru.yandex.inside.goals.model.Goal.Importance;
import ru.yandex.inside.goals.model.Goal.Status;
import ru.yandex.qe.dispenser.api.v1.DiResourcePreorderReasonType;
import ru.yandex.qe.dispenser.api.v1.DiUnit;
import ru.yandex.qe.dispenser.api.v1.request.quota.BaseBody;
import ru.yandex.qe.dispenser.api.v1.request.quota.Body;
import ru.yandex.qe.dispenser.api.v1.request.quota.BodyUpdate;
import ru.yandex.qe.dispenser.api.v1.request.quota.ChangeBody;
import ru.yandex.qe.dispenser.domain.BotCampaignGroup;
import ru.yandex.qe.dispenser.domain.Campaign;
import ru.yandex.qe.dispenser.domain.CampaignForBot;
import ru.yandex.qe.dispenser.domain.GoalQuestionHelper;
import ru.yandex.qe.dispenser.domain.Person;
import ru.yandex.qe.dispenser.domain.Project;
import ru.yandex.qe.dispenser.domain.ProjectFieldsContext;
import ru.yandex.qe.dispenser.domain.QuotaChangeRequest;
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.BaseResourceChange;
import ru.yandex.qe.dispenser.domain.base_resources.WithBaseResourceChanges;
import ru.yandex.qe.dispenser.domain.bot.BigOrder;
import ru.yandex.qe.dispenser.domain.dao.bot.settings.BotCampaignGroupDao;
import ru.yandex.qe.dispenser.domain.dao.campaign.CampaignDao;
import ru.yandex.qe.dispenser.domain.dao.goal.Goal;
import ru.yandex.qe.dispenser.domain.dao.goal.GoalDao;
import ru.yandex.qe.dispenser.domain.dao.segment.SegmentUtils;
import ru.yandex.qe.dispenser.domain.exception.MultiMessageException;
import ru.yandex.qe.dispenser.domain.exception.SingleMessageException;
import ru.yandex.qe.dispenser.domain.hierarchy.Hierarchy;
import ru.yandex.qe.dispenser.domain.hierarchy.PermissionsCache;
import ru.yandex.qe.dispenser.domain.i18n.LocalizableString;
import ru.yandex.qe.dispenser.domain.mds.StorageType;
import ru.yandex.qe.dispenser.domain.util.Errors;
import ru.yandex.qe.dispenser.domain.util.ValidationUtils;
import ru.yandex.qe.dispenser.ws.bot.Provider;
import ru.yandex.qe.dispenser.ws.quota.request.QuotaChangeRequestManager;
import ru.yandex.qe.dispenser.ws.quota.request.owning_cost.ChangeOwningCostContext;
import ru.yandex.qe.dispenser.ws.quota.request.owning_cost.QuotaChangeOwningCostManager;
import ru.yandex.qe.dispenser.ws.quota.request.owning_cost.QuotaChangeRequestOwningCostContext;
import ru.yandex.qe.dispenser.ws.quota.request.owning_cost.formula.ProviderOwningCostFormula;
import ru.yandex.qe.dispenser.ws.quota.request.ticket.QuotaChangeRequestTicketManager;
import ru.yandex.qe.dispenser.ws.quota.request.unbalanced.QuotaChangeUnbalancedManager;
import ru.yandex.qe.dispenser.ws.quota.request.workflow.context.CreateRequestContext;
import ru.yandex.qe.dispenser.ws.quota.request.workflow.context.RequestContext;
import ru.yandex.qe.dispenser.ws.quota.request.workflow.context.UpdateRequestContext;
import ru.yandex.qe.dispenser.ws.quota.request.workflow.context.UpdateResourceRequestContext;
import ru.yandex.qe.dispenser.ws.quota.request.workflow.service.MdbServiceChangesValidatorImpl;
import ru.yandex.qe.dispenser.ws.quota.request.workflow.service.QloudPropertyValidator;
import ru.yandex.qe.dispenser.ws.quota.request.workflow.service.ServiceChangesValidator;
import ru.yandex.qe.dispenser.ws.quota.request.workflow.service.ServiceDictionaryRestrictionManager;
import ru.yandex.qe.dispenser.ws.quota.request.workflow.service.ServicePropertyAllowedValuesValidator;
import ru.yandex.qe.dispenser.ws.quota.request.workflow.service.ServicePropertyValidator;
import ru.yandex.qe.dispenser.ws.quota.request.workflow.service.SimpleServicePropertyValidator;
import ru.yandex.qe.dispenser.ws.quota.request.workflow.service.SolomonPropertyValidator;
import ru.yandex.qe.dispenser.ws.quota.request.workflow.service.YtPropertyValidator;

import static ru.yandex.qe.dispenser.api.v1.DiResourcePreorderReasonType.GROWTH;
import static ru.yandex.qe.dispenser.api.v1.DiResourcePreorderReasonType.NOTHING;

/**
 * {@code ResourceWorkflow} is represent workflow for resource pre-order requests
 */
@SuppressWarnings({"OverlyComplexClass", "OverlyCoupledClass"})
@ParametersAreNonnullByDefault
public class ResourceWorkflow {

    private static final Map<String, ServicePropertyValidator> VALIDATOR_BY_SERVICE_KEY = ImmutableMap.<String, ServicePropertyValidator>builder()
            .put("logbroker", new SimpleServicePropertyValidator(Collections.singleton("account")))
            .put("ydb", new SimpleServicePropertyValidator(Collections.singleton("databaseName")))
            .put("yt", new YtPropertyValidator())
            .put("yp", new ServicePropertyAllowedValuesValidator(ImmutableListMultimap.<String, String>builder()
                    .put("segment", "default")
                    .put("segment", "dev")
                    .build()))
            .put("sqs", new SimpleServicePropertyValidator(Collections.singleton("account")))
            .put("qloud", new QloudPropertyValidator())
            .put("nirvana", new SimpleServicePropertyValidator(Collections.singleton("project")))
            .put("solomon", new SolomonPropertyValidator())
            .build();

    private static final Map<String, ServiceChangesValidator> RESOURCE_VALIDATOR_BY_SERVICE_KEY = ImmutableMap.<String, ServiceChangesValidator>builder()
            .put("dbaas", new MdbServiceChangesValidatorImpl())
            .build();

    private static final Set<QuotaChangeRequest.Status> STATUES_FOR_REVIEW_POPUP_VALIDATION = ImmutableSet.of(
            QuotaChangeRequest.Status.READY_FOR_REVIEW,
            QuotaChangeRequest.Status.NEED_INFO,
            QuotaChangeRequest.Status.CONFIRMED,
            QuotaChangeRequest.Status.APPROVED
    );
    private static final Set<QuotaChangeRequest.Field> REVIEW_POPUP_FIELDS = ImmutableSet.of(
            QuotaChangeRequest.Field.RESOURCE_PREORDER_REASON_TYPE,
            QuotaChangeRequest.Field.REQUEST_GOAL_ANSWERS,
            QuotaChangeRequest.Field.GOAL_ID,
            QuotaChangeRequest.Field.CALCULATION,
            QuotaChangeRequest.Field.CHART_LINKS,
            QuotaChangeRequest.Field.CHART_LINKS_ABSENCE_EXPLANATION,
            QuotaChangeRequest.Field.COMMENT
    );

    private static final ImmutableSet<QuotaChangeRequest.Field> DEFENCE_FIELDS = Sets.immutableEnumSet(
            QuotaChangeRequest.Field.RESOURCE_PREORDER_REASON_TYPE,
            QuotaChangeRequest.Field.GOAL_ID,
            QuotaChangeRequest.Field.DESCRIPTION,
            QuotaChangeRequest.Field.CALCULATION,
            QuotaChangeRequest.Field.CHART_LINKS,
            QuotaChangeRequest.Field.CHART_LINKS_ABSENCE_EXPLANATION,
            QuotaChangeRequest.Field.REQUEST_GOAL_ANSWERS,
            QuotaChangeRequest.Field.COMMENT
    );

    private static final Set<QuotaChangeRequest.Status> READ_ONLY_STATUSES = Sets.immutableEnumSet(
            QuotaChangeRequest.Status.COMPLETED
    );

    private final static Map<String, StorageType> STORAGE_TYPE_BY_RESOURCE = Map.of(
            "storage-s3", StorageType.S3,
            "s3-storage", StorageType.S3,
            "s3_storage", StorageType.S3
    );

    private static final int SUMMARY_LENGTH_LIMIT = 1000;

    private static final Map<QuotaChangeRequest.Status, Set<QuotaChangeRequest.Field>> NOT_UPDATING_TICKET_FIELDS = Map
            .of(QuotaChangeRequest.Status.NEW, DEFENCE_FIELDS);

    private final static Set<Status> VALID_STATUS_SET = Sets.immutableEnumSet(Status.PLANNED, Status.RISK, Status.BLOCKED, Status.NEW,
            Status.UNKNOWN);

    private final static Set<Importance> VALID_IMPORTANCE_SET = Sets.immutableEnumSet(Importance.NOT_KEY, Importance.DEPARTMENT,
            Importance.COMPANY, Importance.OKR);

    public static final String PROPERTY_RESOURCE_PREORDER_ENTITY_KEY = "resource_preorder";

    public static final Set<QuotaChangeRequest.Field> NOT_SENSIBLE_FIELDS = ImmutableSet.<QuotaChangeRequest.Field>builder()
            .addAll(DEFENCE_FIELDS)
            .add(QuotaChangeRequest.Field.SUMMARY)
            .add(QuotaChangeRequest.Field.IMPORTANT_REQUEST)
            .build();

    private final CampaignDao campaignDao;
    private final ServiceDictionaryRestrictionManager serviceDictionaryRestrictionManager;
    private final GoalDao goalDao;
    private final GoalQuestionHelper goalQuestionHelper;
    private final BotCampaignGroupDao campaignGroupDao;
    private final QuotaChangeOwningCostManager quotaChangeOwningCostManager;
    private final QuotaChangeUnbalancedManager quotaChangeUnbalancedManager;
    private final QuotaChangeRequestTicketManager quotaChangeRequestTicketManager;
    private final QuotaChangeRequestManager quotaChangeRequestManager;
    private final AuthorizationManager authorizationManager;

    @SuppressWarnings("ConstructorWithTooManyParameters")
    public ResourceWorkflow(QuotaChangeRequestTicketManager quotaChangeRequestTicketManager,
                            CampaignDao campaignDao,
                            QuotaChangeRequestManager requestManager,
                            ServiceDictionaryRestrictionManager serviceDictionaryRestrictionManager,
                            GoalDao goalDao,
                            GoalQuestionHelper goalQuestionHelper,
                            BotCampaignGroupDao campaignGroupDao,
                            QuotaChangeOwningCostManager quotaChangeOwningCostManager,
                            QuotaChangeUnbalancedManager quotaChangeUnbalancedManager,
                            AuthorizationManager authorizationManager) {
        this.campaignDao = campaignDao;
        this.serviceDictionaryRestrictionManager = serviceDictionaryRestrictionManager;
        this.goalDao = goalDao;
        this.goalQuestionHelper = goalQuestionHelper;
        this.campaignGroupDao = campaignGroupDao;
        this.quotaChangeOwningCostManager = quotaChangeOwningCostManager;
        this.quotaChangeUnbalancedManager = quotaChangeUnbalancedManager;
        this.quotaChangeRequestTicketManager = quotaChangeRequestTicketManager;
        this.quotaChangeRequestManager = requestManager;
        this.authorizationManager = authorizationManager;
    }

    @NotNull
    public WithBaseResourceChanges<List<QuotaChangeRequest>> createRequest(CreateRequestContext ctx) {
        return createRequests(Collections.singletonList(ctx));
    }

    @NotNull
    public WithBaseResourceChanges<List<QuotaChangeRequest>> createRequests(
            Collection<CreateRequestContext> contexts) {
        Map<CreateRequestContext, List<QuotaChangeRequest>> requestsByCtx = new HashMap<>();
        Map<Long, Set<BaseResourceChange>> baseResourceChangesByRequestId = new HashMap<>();
        for (CreateRequestContext ctx : contexts) {
            validateCreateRequestContext(ctx);
            checkUserCanCreateRequest(ctx);
            QuotaChangeRequest.Builder baseRequestBuilder = createRequestBuilder(ctx);
            List<QuotaChangeRequest> requestsToCreate = createRequests(baseRequestBuilder, ctx)
                    .collect(Collectors.toList());
            WithBaseResourceChanges<List<QuotaChangeRequest>> created = quotaChangeRequestManager
                    .create(requestsToCreate, ctx);
            requestsByCtx.put(ctx, created.getValue());
            baseResourceChangesByRequestId.putAll(created.getChangesByQuotaRequest());
        }
        List<QuotaChangeRequest> resultRequests = new ArrayList<>(requestsByCtx.values().size());
        for (CreateRequestContext ctx : contexts) {
            List<QuotaChangeRequest> requestWithTickets = requestsByCtx.get(ctx).stream()
                    .map(req -> createQuotaRequestTicket(req, ctx)).toList();
            resultRequests.addAll(requestWithTickets);
        }
        return new WithBaseResourceChanges<>(resultRequests, baseResourceChangesByRequestId);
    }

    @NotNull
    public WithBaseResourceChanges<QuotaChangeRequest> updateRequest(QuotaChangeRequest request,
                                                                     UpdateRequestContext ctx,
                                                                     boolean suppressSummon) {
        validateCampaignProvidersMode(request, ctx);
        throwErrors(canUserUpdateRequest(request, ctx));
        throwErrors(canRequestBeUpdated(request));
        if (!Sets.intersection(ctx.getChangedFields(), REVIEW_POPUP_FIELDS).isEmpty()) {
            throwErrors(canReviewPopupBeUpdated(request));
        }
        validateChangesForUpdate(request, ctx);
        Set<QuotaChangeRequest.Field> changedFields = ctx.getChangedFields();
        QuotaChangeRequest.Builder updateBuilder = ctx.getUpdatedRequest().copyBuilder();
        WithBaseResourceChanges<QuotaChangeRequest> updatedRequest;
        if (changedFields.contains(QuotaChangeRequest.Field.CHANGES)) {
            updatedRequest = quotaChangeRequestManager.updateWithChanges(updateBuilder, request, ctx.getChanges(),
                    changedFields, ctx, true);
        } else if (!changedFields.isEmpty()) {
            updatedRequest = quotaChangeRequestManager.update(updateBuilder, request, changedFields, ctx, true);
        } else {
            updatedRequest = quotaChangeRequestManager.getBaseResourceChanges(request);
        }
        afterUpdateRequest(request, updatedRequest.getValue(), changedFields, ctx, suppressSummon);
        return updatedRequest;
    }

    @NotNull
    public QuotaChangeRequest setStatus(QuotaChangeRequest request, QuotaChangeRequest.Status targetStatus,
                                        RequestContext ctx, boolean suppressSummon) {
        QuotaChangeRequest.Status currentStatus = request.getStatus();
        if (targetStatus != currentStatus) {
            Map<QuotaChangeRequest.Status, Set<QuotaChangeRequest.Status>> allowedTransitions = authorizationManager
                    .allowedTransitionsInCampaignType(ctx.getPerson(), request.getProject(),
                            Objects.requireNonNull(request.getCampaign()).getType());
            Collection<QuotaChangeRequest.Status> validStatuses = getValidStatuses(request, allowedTransitions);
            if (!validStatuses.contains(targetStatus)) {
                if (!validStatuses.isEmpty()) {
                    String validStatusesString = validStatuses.stream()
                            .map(Enum::name)
                            .sorted()
                            .collect(Collectors.joining(", "));
                    throw SingleMessageException.illegalArgument("cannot.change.status.with.available", targetStatus.name(), validStatusesString);
                } else {
                    throw SingleMessageException.illegalArgument("cannot.change.status.no.available", targetStatus.name());
                }
            }
            applyStatusChange(request, targetStatus, allowedTransitions);
            return quotaChangeRequestManager.setStatus(request, targetStatus, ctx, suppressSummon);
        }
        return request;
    }

    public CreateRequestContext buildCreateRequestContext(Body body, Person author, @NotNull Campaign campaign) {
        return buildCreateRequestContext(Collections.singleton(body), author, campaign).iterator().next();
    }

    public Collection<CreateRequestContext> buildCreateRequestContext(Collection<Body> bodies, Person author, @NotNull Campaign campaign) {
        BotCampaignGroup campaignGroup = campaignGroupDao.getByCampaign(campaign.getId()).orElse(null);
        Map<Long, Campaign.BigOrder> bigOrderById = campaign.getBigOrders().stream()
                .collect(Collectors.toMap(Campaign.BigOrder::getBigOrderId, Function.identity()));
        Map<Long, Goal> goals = goalDao.read(bodies.stream()
                .map(BaseBody::getGoalId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet()));
        ArrayList<CreateRequestContext> contexts = new ArrayList<>(bodies.size());
        for (Body body : bodies) {
            Project project = Hierarchy.get().getProjectReader().readExisting(body.getProjectKey());
            List<ChangeBody> changes = body.getChanges();
            List<QuotaChangeRequest.Change> parsedChanges = parseChanges(bigOrderById, changes);
            Goal goal = null;
            if (body.getGoalId() != null) {
                goal = goals.get(body.getGoalId());
            }
            CreateRequestContext ctx = new CreateRequestContext(author, project, parsedChanges, body, goal,
                    QuotaChangeRequest.Campaign.from(campaign), campaignGroup);
            contexts.add(ctx);
        }
        return contexts;
    }

    public UpdateRequestContext buildUpdateRequestContext(QuotaChangeRequest request, BodyUpdate body, Person author) {
        Campaign campaign = campaignDao.read(Objects.requireNonNull(request.getCampaignId()));
        Optional<BotCampaignGroup> campaignGroup = campaignGroupDao.getByCampaign(request.getCampaignId());
        List<? extends QuotaChangeRequest.ChangeAmount> changes;
        if (body.getChanges() != null) {
            Map<Long, Campaign.BigOrder> bigOrderById = campaign.getBigOrders().stream()
                    .collect(Collectors.toMap(Campaign.BigOrder::getBigOrderId, Function.identity()));
            changes = parseChanges(bigOrderById, body.getChanges());
        } else {
            changes = Collections.emptyList();
        }
        Goal goal = null;
        if (body.getResourcePreorderReasonType() != GROWTH) {
            goal = request.getGoal();
        }
        if (body.getGoalId() != null) {
            if (request.getGoal() == null || request.getGoal().getId() != body.getGoalId()) {
                goal = goalDao.read(body.getGoalId());
            }
        }
        return new UpdateResourceRequestContext(author, request, changes, body, goal, request.getCampaign(), campaignGroup.orElse(null));
    }

    public boolean canUserCreateRequestByProject(Person person,
                                                 Project project,
                                                 ProjectFieldsContext projectFieldsContext) {
        if (project.getPathToRoot().stream().anyMatch(p -> Project.TRASH_PROJECT_KEY.equals(p.getPublicKey()))) {
            return false;
        }
        List<Campaign> activeCampaigns = projectFieldsContext.getActiveCampaigns().stream()
                .filter(c -> c.getStatus() == Campaign.Status.ACTIVE && !c.isRequestCreationDisabled()).toList();
        if (activeCampaigns.isEmpty()) {
            return false;
        }
        Set<Campaign.Type> activeCampaignTypes = activeCampaigns.stream().map(Campaign::getType)
                .collect(Collectors.toSet());
        return authorizationManager.hasCreatePermissionsInAnyOfCampaignTypes(person, project, activeCampaignTypes);
    }

    public List<Campaign> getEligibleCampaigns(Person person,
                                               Project project) {
        if (project.getPathToRoot().stream().anyMatch(p -> Project.TRASH_PROJECT_KEY.equals(p.getPublicKey()))) {
            return List.of();
        }
        List<Campaign> activeCampaigns = campaignDao.getAllActiveSorted().stream()
                .filter(c -> c.getStatus() == Campaign.Status.ACTIVE && !c.isRequestCreationDisabled()).toList();
        if (activeCampaigns.isEmpty()) {
            return List.of();
        }
        Set<Campaign.Type> activeCampaignTypes = activeCampaigns.stream().map(Campaign::getType)
                .collect(Collectors.toSet());
        Map<Campaign.Type, Boolean> availabilityByCampaignType = authorizationManager
                .hasCreatePermissionsInCampaignTypes(person, project, activeCampaignTypes);
        return activeCampaigns.stream().filter(c -> availabilityByCampaignType.get(c.getType())).toList();
    }

    public Errors<LocalizableString> canRequestBeUpdated(QuotaChangeRequest quotaChangeRequest) {
        Errors<LocalizableString> canUpdateRequestError = new Errors<>();
        if (isFinalStatus(quotaChangeRequest.getStatus())) {
            canUpdateRequestError.add(LocalizableString.of("request.is.in.final.status"));
        }
        if (isUpdateForbiddenInCampaign(quotaChangeRequest)) {
            canUpdateRequestError.add(LocalizableString.of("request.cannot.be.updated.at.campaign.stage"));
            return canUpdateRequestError;
        }
        if (isRequestProjectInTrash(quotaChangeRequest)) {
            canUpdateRequestError.add(LocalizableString.of("request.cannot.be.updated.in.wrong.project"));
            return canUpdateRequestError;
        }
        if (isOrderNotInCampaign(quotaChangeRequest)) {
            canUpdateRequestError.add(LocalizableString.of("request.cannot.be.updated.for.wrong.big.order"));
            return canUpdateRequestError;
        }
        return canUpdateRequestError;
    }

    public Errors<LocalizableString> canUserUpdateRequest(QuotaChangeRequest quotaChangeRequest, RequestContext ctx) {
        Errors<LocalizableString> canUpdateRequestError = new Errors<>();
        boolean canUpdate = authorizationManager.hasUpdatePermissionsInCampaignType(ctx.getPerson(), ctx.getProject(),
                Objects.requireNonNull(quotaChangeRequest.getCampaign()).getType());
        if (!canUpdate) {
            canUpdateRequestError.add(LocalizableString.of("not.enough.permissions.to.modify.request.in.campaign"));
        }
        return canUpdateRequestError;
    }

    @NotNull
    public Collection<QuotaChangeRequest.Status> getValidStatuses(QuotaChangeRequest request, RequestContext ctx) {
        Map<QuotaChangeRequest.Status, Set<QuotaChangeRequest.Status>> allowedTransitions = authorizationManager
                .allowedTransitionsInCampaignType(ctx.getPerson(), request.getProject(),
                        Objects.requireNonNull(request.getCampaign()).getType());
        return getValidStatuses(request, allowedTransitions);
    }

    @SuppressWarnings("OverlyComplexMethod")
    @NotNull
    public Collection<QuotaChangeRequest.Status> getValidStatuses(
            QuotaChangeRequest request, Map<QuotaChangeRequest.Status, Set<QuotaChangeRequest.Status>> allowedTransitions) {
        QuotaChangeRequest.Status status = request.getStatus();
        ImmutableSet.Builder<QuotaChangeRequest.Status> resultBuilder = ImmutableSet.builder();
        if (validateTransitionsAvailable(request).hasErrors()) {
            return resultBuilder.build();
        }
        if (status == QuotaChangeRequest.Status.CANCELLED) {
            if (transitionAllowed(allowedTransitions, QuotaChangeRequest.Status.CANCELLED, QuotaChangeRequest.Status.NEW)) {
                resultBuilder.add(QuotaChangeRequest.Status.NEW);
            }
        } else if (status == QuotaChangeRequest.Status.REJECTED) {
            if (transitionAllowed(allowedTransitions, QuotaChangeRequest.Status.REJECTED, QuotaChangeRequest.Status.NEW)) {
                resultBuilder.add(QuotaChangeRequest.Status.NEW);
            }
        } else if (status == QuotaChangeRequest.Status.NEW) {
            if (transitionAllowed(allowedTransitions, QuotaChangeRequest.Status.NEW, QuotaChangeRequest.Status.READY_FOR_REVIEW)) {
                resultBuilder.add(QuotaChangeRequest.Status.READY_FOR_REVIEW);
            }
            if (!canRequestBeCancelled(request).hasErrors() && transitionAllowed(allowedTransitions,
                    QuotaChangeRequest.Status.NEW, QuotaChangeRequest.Status.CANCELLED)) {
                resultBuilder.add(QuotaChangeRequest.Status.CANCELLED);
            }
        } else if (status == QuotaChangeRequest.Status.CONFIRMED) {
            if (!canRequestBeRejected(request).hasErrors() && transitionAllowed(allowedTransitions,
                    QuotaChangeRequest.Status.CONFIRMED, QuotaChangeRequest.Status.REJECTED)) {
                resultBuilder.add(QuotaChangeRequest.Status.REJECTED);
            }
        } else if (status == QuotaChangeRequest.Status.READY_FOR_REVIEW) {
            if (!canRequestBeRejected(request).hasErrors() && transitionAllowed(allowedTransitions,
                    QuotaChangeRequest.Status.READY_FOR_REVIEW, QuotaChangeRequest.Status.REJECTED)) {
                resultBuilder.add(QuotaChangeRequest.Status.REJECTED);
            }
            if (transitionAllowed(allowedTransitions, QuotaChangeRequest.Status.READY_FOR_REVIEW, QuotaChangeRequest.Status.APPROVED)) {
                resultBuilder.add(QuotaChangeRequest.Status.APPROVED);
            }
            if (transitionAllowed(allowedTransitions, QuotaChangeRequest.Status.READY_FOR_REVIEW, QuotaChangeRequest.Status.NEED_INFO)) {
                resultBuilder.add(QuotaChangeRequest.Status.NEED_INFO);
            }
            if (!canRequestBeCancelled(request).hasErrors() && transitionAllowed(allowedTransitions,
                    QuotaChangeRequest.Status.READY_FOR_REVIEW, QuotaChangeRequest.Status.CANCELLED)) {
                resultBuilder.add(QuotaChangeRequest.Status.CANCELLED);
            }
        } else if (status == QuotaChangeRequest.Status.APPROVED) {
            if (transitionAllowed(allowedTransitions, QuotaChangeRequest.Status.APPROVED, QuotaChangeRequest.Status.CONFIRMED)) {
                resultBuilder.add(QuotaChangeRequest.Status.CONFIRMED);
            }
            if (transitionAllowed(allowedTransitions, QuotaChangeRequest.Status.APPROVED, QuotaChangeRequest.Status.NEED_INFO)) {
                resultBuilder.add(QuotaChangeRequest.Status.NEED_INFO);
            }
            if (!canRequestBeRejected(request).hasErrors() && transitionAllowed(allowedTransitions,
                    QuotaChangeRequest.Status.APPROVED, QuotaChangeRequest.Status.REJECTED)) {
                resultBuilder.add(QuotaChangeRequest.Status.REJECTED);
            }
            if (!canRequestBeCancelled(request).hasErrors() && transitionAllowed(allowedTransitions,
                    QuotaChangeRequest.Status.APPROVED, QuotaChangeRequest.Status.CANCELLED)) {
                resultBuilder.add(QuotaChangeRequest.Status.CANCELLED);
            }
        } else if (status == QuotaChangeRequest.Status.NEED_INFO) {
            if (transitionAllowed(allowedTransitions, QuotaChangeRequest.Status.NEED_INFO, QuotaChangeRequest.Status.APPROVED)) {
                resultBuilder.add(QuotaChangeRequest.Status.APPROVED);
            }
            if (!canRequestBeRejected(request).hasErrors() && transitionAllowed(allowedTransitions,
                    QuotaChangeRequest.Status.NEED_INFO, QuotaChangeRequest.Status.REJECTED)) {
                resultBuilder.add(QuotaChangeRequest.Status.REJECTED);
            }
            if (transitionAllowed(allowedTransitions, QuotaChangeRequest.Status.NEED_INFO, QuotaChangeRequest.Status.READY_FOR_REVIEW)) {
                resultBuilder.add(QuotaChangeRequest.Status.READY_FOR_REVIEW);
            }
            if (!canRequestBeCancelled(request).hasErrors() && transitionAllowed(allowedTransitions,
                    QuotaChangeRequest.Status.NEED_INFO, QuotaChangeRequest.Status.CANCELLED)) {
                resultBuilder.add(QuotaChangeRequest.Status.CANCELLED);
            }
        }
        return resultBuilder.build();
    }

    public Errors<LocalizableString> canReviewPopupBeUpdated(QuotaChangeRequest quotaChangeRequest) {
        Errors<LocalizableString> errors = new Errors<>();
        if (quotaChangeRequest.getResourcePreorderReasonType() == NOTHING) {
            errors.add(LocalizableString.of("review.fields.cannot.be.updated.for.reason",
                    quotaChangeRequest.getResourcePreorderReasonType().name()));
            return errors;
        }
        return errors;
    }

    public static boolean canUserViewMoney(Person person) {
        return Hierarchy.get().getPermissionsCache().canUserViewMoney(person);
    }

    public static boolean canUserViewBotMoney(Person person) {
        return Hierarchy.get().getPermissionsCache().canUserViewBotMoney(person);
    }

    public static boolean isUserProcessResponsibleOrProvidersAdmin(Person person) {
        return PermissionsCache.isUserProcessResponsibleOrProvidersAdmin(Hierarchy.get().getProjectReader(),
                Hierarchy.get().getServiceReader(), person);
    }

    public static boolean isUserProcessResponsible(Person person) {
        return PermissionsCache.isUserProcessResponsible(Hierarchy.get().getProjectReader(), person);
    }

    private void applyStatusChange(QuotaChangeRequest request, QuotaChangeRequest.Status status,
                                   Map<QuotaChangeRequest.Status, Set<QuotaChangeRequest.Status>> allowedTransitions) {
        throwErrors(validateTransitionsAvailable(request));
        QuotaChangeRequest.Status currentStatus = request.getStatus();
        switch (status) {
            case CANCELLED:
                throwErrors(canRequestBeCancelled(request));
                break;
            case REJECTED:
                throwErrors(canRequestBeRejected(request));
                break;
            case READY_FOR_REVIEW:
            case APPROVED:
            case NEED_INFO:
            case CONFIRMED:
            case NEW:
                break;
            default:
                throw SingleMessageException.illegalArgument("invalid.workflow.status");
        }
        if (!transitionAllowed(allowedTransitions, currentStatus, status)) {
            throw SingleMessageException.illegalArgument("not.enough.permissions.to.transition.request.in.campaign");
        }
    }

    private Errors<LocalizableString> validateTransitionsAvailable(QuotaChangeRequest request) {
        if (isStatusChangeForbiddenInCampaign(request)) {
            return Errors.of(LocalizableString.of("transition.is.not.available.in.this.campaign"));
        }
        if (isRequestProjectInTrash(request)) {
            return Errors.of(LocalizableString.of("transition.is.not.available.for.this.service"));
        }
        if (isOrderNotInCampaign(request)) {
            return Errors.of(LocalizableString.of("transition.is.not.available.for.wrong.big.order"));
        }
        return new Errors<>();
    }

    private Errors<LocalizableString> canRequestBeRejected(QuotaChangeRequest request) {
        if (isQuotaDeliveryInProgress(request)) {
            return Errors.of(LocalizableString.of("request.cannot.be.rejected.while.quota.delivery.in.progress"));
        }
        return new Errors<>();
    }

    private void validateChangesForUpdate(QuotaChangeRequest request, UpdateRequestContext ctx) {
        if (ctx.isChangedField(QuotaChangeRequest.Field.PROJECT)) {
            List<? extends QuotaChangeRequest.ChangeAmount> changes;
            if (ctx.isChangedField(QuotaChangeRequest.Field.CHANGES)) {
                changes = ctx.getChanges();
            } else {
                changes = request.getChanges();
            }
            boolean hasMdsChanges = changes.stream()
                    .anyMatch(c -> STORAGE_TYPE_BY_RESOURCE.containsKey(c.getResource().getPublicKey()) ||
                            Provider.MDS.is(c.getResource().getService()));
            if (hasMdsChanges) {
                throw SingleMessageException.illegalArgument("cannot.update.abc.project.with.mds.changes");
            }
            if (isProjectInTrash(ctx.getUpdatedRequest().getProject())) {
                throw SingleMessageException.illegalArgument("cannot.update.abc.project.from.trash");
            }
        }
        Long goalId = ctx.getUpdatedRequest().getGoal() != null ? ctx.getUpdatedRequest().getGoal().getId() : null;
        if (ctx.isChangedField(QuotaChangeRequest.Field.GOAL_ID) && ctx.getUpdatedRequest().getGoal() != null) {
            throwErrors(validateGoal(ctx.getUpdatedRequest().getGoal()));
        }
        DiResourcePreorderReasonType resourcePreorderReasonType = ctx.getUpdatedRequest().getResourcePreorderReasonType();
        throwErrors(validateResourcePreorderReasonType(resourcePreorderReasonType, goalId));
        if (ctx.isChangedField(QuotaChangeRequest.Field.REQUEST_GOAL_ANSWERS)) {
            Map<Long, String> requestGoalAnswers = Objects.requireNonNull(ctx.getUpdatedRequest().getRequestGoalAnswers());
            DiResourcePreorderReasonType diResourcePreorderReasonType = ctx.getUpdatedRequest().getResourcePreorderReasonType();
            if (diResourcePreorderReasonType == null) {
                if (!requestGoalAnswers.isEmpty()) {
                    throw SingleMessageException.illegalArgument("wrong.answers");
                }
            } else {
                validateDraftAnswers(diResourcePreorderReasonType, requestGoalAnswers);
            }
        }
        if (ctx.isChangedField(QuotaChangeRequest.Field.CHART_LINKS)
                || ctx.isChangedField(QuotaChangeRequest.Field.CHART_LINKS_ABSENCE_EXPLANATION)) {
            if (!StringUtils.isEmpty(ctx.getUpdatedRequest().getChartLinksAbsenceExplanation()) &&
                    !ctx.getUpdatedRequest().getChartLinks().isEmpty()) {
                throw SingleMessageException.illegalArgument("either.chart.links.field.allowed");
            }
        }
        if (ctx.isChangedField(QuotaChangeRequest.Field.CHANGES)) {
            validateChanges(ctx, ctx.getChanges());
        }
        if (ctx.isChangedField(QuotaChangeRequest.Field.CHART_LINKS)) {
            List<String> chartLinks = ctx.getUpdatedRequest().getChartLinks();
            for (String chartLink : chartLinks) {
                if (!ValidationUtils.isValidURL(chartLink)) {
                    throw SingleMessageException.illegalArgument("invalid.chart.link", chartLink);
                }
            }
        }
        if (ctx.isChangedField(QuotaChangeRequest.Field.SUMMARY)) {
            String summary = Objects.requireNonNull(ctx.getUpdatedRequest().getSummary());
            if (summary.length() > SUMMARY_LENGTH_LIMIT) {
                throw SingleMessageException.illegalArgument("summary.must.be.shorter.than", SUMMARY_LENGTH_LIMIT);
            }
            if (summary.trim().isEmpty()) {
                throw SingleMessageException.illegalArgument("non.blank.summary.required");
            }
        }
        if (ctx.isChangedField(QuotaChangeRequest.Field.ADDITIONAL_PROPERTIES)) {
            Map<String, String> additionalProperties = Objects.requireNonNull(ctx.getUpdatedRequest().getAdditionalProperties());
            Map<String, String> currentAdditionalProperties = request.getAdditionalProperties() != null
                    ? request.getAdditionalProperties() : Collections.emptyMap();
            validateAdditionalPropertiesOnUpdate(ctx, additionalProperties, currentAdditionalProperties);
        }
        if (ctx.getCampaignGroup() != null && ctx.getCampaign() != null && !ctx.isOnlyDecrease()) {
            List<CampaignForBot> activeCampaigns = ctx.getCampaignGroup().getActiveCampaigns();
            Set<Long> activeBigOrderIds = activeCampaigns.isEmpty() ? Collections.emptySet() :
                    activeCampaigns.stream().flatMap(c -> c.getBigOrders().stream()
                            .map(BigOrder::getId))
                            .collect(Collectors.toSet());
            Map<QuotaChangeRequest.ChangeKey, Long> oldChangesFonInactiveBigOrders = request.getChanges().stream()
                    .filter(c -> !activeBigOrderIds.contains(c.getKey().getBigOrder().getId()))
                    .collect(Collectors.toMap(QuotaChangeRequest.ChangeAmount::getKey, QuotaChangeRequest.ChangeAmount::getAmount));
            Map<QuotaChangeRequest.ChangeKey, QuotaChangeRequest.ChangeAmount> newChangesFonInactiveBigOrders = ctx.getChanges().stream()
                    .filter(c -> !activeBigOrderIds.contains(c.getKey().getBigOrder().getId()))
                    .collect(Collectors.toMap(QuotaChangeRequest.ChangeAmount::getKey, Function.identity()));
            Errors<LocalizableString> errors = new Errors<>();
            for (QuotaChangeRequest.ChangeKey changeKey : newChangesFonInactiveBigOrders.keySet()) {
                QuotaChangeRequest.ChangeAmount newChange = newChangesFonInactiveBigOrders.get(changeKey);
                Long oldAmount = oldChangesFonInactiveBigOrders.getOrDefault(changeKey, 0L);
                if (newChange.getAmount() > oldAmount) {
                    errors.add(LocalizableString.of("cannot.update.change.amount.greater.than.ordered",
                            newChange.getResource().getName(),
                            newChange.getKey().getBigOrder().getDate().format(DateTimeFormatter.ISO_LOCAL_DATE)));
                }
            }
            throwErrors(errors);
        }
    }

    private void afterUpdateRequest(QuotaChangeRequest request, QuotaChangeRequest updatedRequest,
                                    Set<QuotaChangeRequest.Field> changedFields,
                                    UpdateRequestContext ctx, boolean suppressSummon) {
        if (STATUES_FOR_REVIEW_POPUP_VALIDATION.contains(updatedRequest.getStatus()) && !Collections.disjoint(changedFields, DEFENCE_FIELDS)) {
            quotaChangeRequestManager.addRequestGoalDataComments(updatedRequest, ctx);
        }
        if (!Sets.difference(changedFields, NOT_UPDATING_TICKET_FIELDS.getOrDefault(updatedRequest.getStatus(), Collections.emptySet())).isEmpty()) {
            quotaChangeRequestTicketManager.updateTicket(updatedRequest, changedFields, request, ctx, suppressSummon);
        }
    }

    private void validateCreateRequestContext(CreateRequestContext ctx) {
        if (ctx.getChanges().isEmpty()) {
            throw SingleMessageException.illegalArgument("cannot.create.request.without.changes");
        }
        String summary = ctx.getBody().getSummary();
        if (summary != null) {
            if (summary.length() > SUMMARY_LENGTH_LIMIT) {
                throw SingleMessageException.illegalArgument("summary.must.be.shorter.than", SUMMARY_LENGTH_LIMIT);
            }
            if (summary.trim().isEmpty()) {
                throw SingleMessageException.illegalArgument("non.blank.summary.required");
            }
        }
        boolean projectInTrash = ctx.getProject().getPathToRoot().stream()
                .anyMatch(p -> Project.TRASH_PROJECT_KEY.equals(p.getPublicKey()));
        if (projectInTrash) {
            throw SingleMessageException.illegalArgument("request.cannot.be.created.in.wrong.project");
        }
        DiResourcePreorderReasonType resourcePreorderReasonType = ctx.getBody().getResourcePreorderReasonType();
        Long goalId = ctx.getBody().getGoalId();
        throwErrors(validateResourcePreorderReasonType(resourcePreorderReasonType, goalId));
        List<String> chartLinks = ctx.getBody().getChartLinks();
        if (chartLinks != null) {
            for (String chartLink : chartLinks) {
                if (!ValidationUtils.isValidURL(chartLink)) {
                    throw SingleMessageException.illegalArgument("invalid.chart.link", chartLink);
                }
            }
        }
        if (ctx.getGoal() != null) {
            throwErrors(validateGoal(ctx.getGoal()));
        }
        QuotaChangeRequest.Campaign campaign = ctx.getCampaign();
        if (campaign.isSingleProviderRequestModeEnabled()) {
            if (ctx.getServices().size() > 1) {
                throw SingleMessageException.illegalArgument("requests.with.multiple.services.cannot.be.created.in.campaign", campaign.getKey());
            }
            long bigOrdersCount = ctx.getChanges().stream()
                    .map(c -> c.getKey().getBigOrder().getId())
                    .distinct()
                    .count();
            if (bigOrdersCount > 1) {
                throw SingleMessageException.illegalArgument("requests.with.multiple.big.orders.cannot.be.created.in.campaign", campaign.getKey());
            }
        }
        if (ctx.getBody().getSummary() == null) {
            throw SingleMessageException.illegalArgument("summary.required");
        }
    }

    private void checkUserCanCreateRequest(CreateRequestContext ctx) {
        if (ctx.getCampaign() == null) {
            throw new IllegalArgumentException("Campaign is required to create quota request");
        }
        QuotaChangeRequest.Campaign campaign = ctx.getCampaign();
        if (campaign.isDeleted()) {
            throw SingleMessageException.illegalArgument("campaign.not.found");
        }
        if (campaign.getStatus() != Campaign.Status.ACTIVE) {
            throw SingleMessageException.illegalArgument("campaign.is.inactive");
        }
        if (campaign.isRequestCreationDisabled()) {
            throw SingleMessageException.illegalArgument("request.creation.disabled");
        }
        boolean available = authorizationManager
                .hasCreatePermissionsInCampaignType(ctx.getPerson(), ctx.getProject(), campaign.getType());
        if (!available) {
            throw SingleMessageException.illegalArgument("not.enough.permissions.to.create.request.in.campaign");
        }
    }

    @NotNull
    private List<QuotaChangeRequest.Change> parseChanges(Map<Long, Campaign.BigOrder> bigOrderById, List<ChangeBody> changes) {
        Map<QuotaChangeRequest.ChangeKey, Long> amountByKey = new HashMap<>();
        for (ChangeBody change : changes) {
            Service service = Hierarchy.get().getServiceReader().read(change.getServiceKey());
            Resource resource = Hierarchy.get().getResourceReader().read(new Resource.Key(change.getResourceKey(), service));
            Set<Segment> segments = SegmentUtils.getCompleteSegmentSet(resource, change.getSegmentKeys());
            if (change.getOrderId() == null) {
                throw SingleMessageException.illegalArgument("big.order.id.required");
            }
            Campaign.BigOrder campaignBigOrder = bigOrderById.get(change.getOrderId());
            if (campaignBigOrder == null) {
                throw SingleMessageException.illegalArgument("big.order.is.not.present.in.current.campaign", change.getOrderId());
            }
            QuotaChangeRequest.BigOrder bigOrder = new QuotaChangeRequest.BigOrder(campaignBigOrder.getBigOrderId(), campaignBigOrder.getDate(), true);
            boolean hasAggregationSegments = segments.stream().anyMatch(Segment::isAggregationSegment);
            if (hasAggregationSegments) {
                throw SingleMessageException.illegalArgument("invalid.segment.set");
            }
            DiUnit baseUnit = resource.getType().getBaseUnit();
            long amount = baseUnit.convert(change.getAmount());
            QuotaChangeRequest.ChangeKey key = new QuotaChangeRequest.ChangeKey(bigOrder, resource, segments);
            amountByKey.put(key, amountByKey.getOrDefault(key, 0L) + amount);
        }
        List<QuotaChangeRequest.Change> parsedChanges = new ArrayList<>(amountByKey.size());
        for (QuotaChangeRequest.ChangeKey changeKey : amountByKey.keySet()) {
            parsedChanges.add(
                    QuotaChangeRequest.Change.newChangeBuilder()
                            .key(changeKey)
                            .amount(amountByKey.get(changeKey))
                            .build()
            );
        }
        return parsedChanges;
    }

    private QuotaChangeRequest.Builder createRequestBuilder(CreateRequestContext ctx) {
        DiResourcePreorderReasonType resourcePreorderReasonType = ctx.getBody().getResourcePreorderReasonType() == null
                ? GROWTH
                : ctx.getBody().getResourcePreorderReasonType();
        boolean unbalanced = quotaChangeUnbalancedManager.calculateForNewRequest(ctx);
        long now = System.currentTimeMillis();
        validateChanges(ctx, ctx.getChanges());
        Map<String, String> additionalProperties = ctx.getBody().getAdditionalProperties();
        if (additionalProperties != null) {
            validateAdditionalPropertiesOnCreate(ctx, additionalProperties);
        }
        return new QuotaChangeRequest.Builder()
                .status(QuotaChangeRequest.Status.NEW)
                .description(ctx.getBody().getDescription())
                .comment(ctx.getBody().getComment())
                .calculations(ctx.getBody().getCalculations())
                .created(now)
                .updated(now)
                .author(ctx.getPerson())
                .type(QuotaChangeRequest.Type.of(ctx.getBody().getType()))
                .chartLinks(ctx.getBody().getChartLinks() != null ? ctx.getBody().getChartLinks() : Collections.emptyList())
                .chartLinksAbsenceExplanation(ctx.getBody().getChartLinksAbsenceExplanation())
                .additionalProperties(additionalProperties)
                .cost(0.0)
                .requestOwningCost(0L)
                .summary(ctx.getBody().getSummary())
                .importantRequest(ctx.getBody().getImportantRequest() != null && ctx.getBody().getImportantRequest())
                .campaign(ctx.getCampaign())
                .resourcePreorderReasonType(resourcePreorderReasonType)
                .goal(ctx.getGoal())
                .unbalanced(unbalanced)
                .campaignType(ctx.getCampaign() != null ? ctx.getCampaign().getType() : null);
    }

    private void validateCampaignProvidersMode(QuotaChangeRequest quotaChangeRequest, UpdateRequestContext ctx) {
        QuotaChangeRequest.Campaign campaign = quotaChangeRequest.getCampaign();
        if (campaign.isSingleProviderRequestModeEnabled() && !ctx.getChanges().isEmpty()) {
            long servicesCount = ctx.getChanges().stream()
                    .map(c -> c.getResource().getService())
                    .distinct()
                    .count();
            if (servicesCount > 1) {
                throw SingleMessageException.illegalArgument("requests.with.multiple.services.cannot.be.created.in.campaign", campaign.getKey());
            }
            long bigOrdersCount = ctx.getChanges().stream()
                    .map(c -> c.getKey().getBigOrder().getId())
                    .distinct()
                    .count();
            if (bigOrdersCount > 1) {
                throw SingleMessageException.illegalArgument("requests.with.multiple.big.orders.cannot.be.created.in.campaign", campaign.getKey());
            }
        }
    }

    private Stream<QuotaChangeRequest> createRequests(QuotaChangeRequest.Builder baseRequestBuilder,
                                                      CreateRequestContext ctx) {
        List<ChangeOwningCostContext> changeOwningCostContexts = ctx.getChanges().stream()
                .map(change -> ChangeOwningCostContext.builder()
                        .change(change)
                        .abcServiceId(ctx.getProject().getAbcServiceId())
                        .campaign(ctx.getCampaign())
                        .build())
                .collect(Collectors.toList());
        List<QuotaChangeRequest.Change> changes = quotaChangeOwningCostManager.getChangesWithCalculatedOwningCost(
                QuotaChangeRequestOwningCostContext.builder()
                        .changeOwningCostContexts(changeOwningCostContexts)
                        .mode(QuotaChangeRequestOwningCostContext.Mode.CREATE)
                        .build());
        Long requestOwningCost = changes.stream()
                .map(change -> ProviderOwningCostFormula.owningCostToOutputFormat(change.getOwningCost()))
                .reduce(BigDecimal::add)
                .map(ProviderOwningCostFormula::owningCostToOutputFormat)
                .map(BigDecimal::longValueExact)
                .orElse(0L);
        return Stream.of(baseRequestBuilder.project(ctx.getProject())
                .changes(changes)
                .requestOwningCost(requestOwningCost)
                .build());
    }

    private Errors<LocalizableString> canRequestBeCancelled(QuotaChangeRequest quotaChangeRequest) {
        if (isQuotaDeliveryInProgress(quotaChangeRequest)) {
            return Errors.of(LocalizableString.of("request.cannot.be.cancelled.while.quota.delivery.in.progress"));
        }
        return new Errors<>();
    }

    private boolean isRequestProjectInTrash(QuotaChangeRequest quotaChangeRequest) {
        return isProjectInTrash(quotaChangeRequest.getProject());
    }

    private boolean isProjectInTrash(Project project) {
        return project.getPathToRoot().stream()
                .anyMatch(p -> Project.TRASH_PROJECT_KEY.equals(p.getPublicKey()));
    }

    private boolean isStatusChangeForbiddenInCampaign(QuotaChangeRequest request) {
        QuotaChangeRequest.Campaign campaign = request.getCampaign();
        if (campaign == null || campaign.isDeleted()) {
            return true;
        }
        return isUpdateForbiddenInCampaign(request);
    }

    private boolean isUpdateForbiddenInCampaign(QuotaChangeRequest request) {
        return request.getCampaign() == null
                || request.getCampaign().isDeleted()
                || request.getCampaign().isRequestModificationDisabled()
                || (request.getCampaign().getStatus() != Campaign.Status.ACTIVE
                        && !request.getCampaign().isAllowedRequestModificationWhenClosed());
    }

    private boolean isOrderNotInCampaign(QuotaChangeRequest request) {
        return request.getChanges().stream().anyMatch(c -> c.getKey().getBigOrder() == null || !c.getKey().getBigOrder().isInCampaign());
    }

    private boolean isQuotaDeliveryInProgress(QuotaChangeRequest request) {
        return request.getChanges().stream().anyMatch(c -> c.getAmountReady() > 0 || c.getAmountAllocated() > 0);
    }

    private void validateAdditionalPropertiesOnCreate(@NotNull RequestContext context,
                                                      @NotNull Map<String, String> additionalProperties) {

        Set<Service> services = context.getServices();
        if (services.size() > 1) {
            throw SingleMessageException.illegalArgument("cannot.validate.multi.service.properties");
        }
        Service service = services.iterator().next();
        ServicePropertyValidator validator = VALIDATOR_BY_SERVICE_KEY.get(service.getKey());
        if (validator != null) {
            validator.validateAdditionalPropertiesOnCreate(context, additionalProperties);
        }
    }

    private void validateAdditionalPropertiesOnUpdate(@NotNull RequestContext context,
                                                      @NotNull Map<String, String> additionalProperties,
                                                      @NotNull Map<String, String> currentAdditionalProperties) {

        Set<Service> services = context.getServices();
        if (services.size() > 1) {
            throw SingleMessageException.illegalArgument("cannot.validate.multi.service.properties");
        }
        Service service = services.iterator().next();
        ServicePropertyValidator validator = VALIDATOR_BY_SERVICE_KEY.get(service.getKey());
        if (validator != null) {
            validator.validateAdditionalPropertiesOnUpdate(context, additionalProperties, currentAdditionalProperties);
        }
    }

    private void validateChanges(@NotNull RequestContext context,
                                 @NotNull List<? extends QuotaChangeRequest.ChangeAmount> changes) {
        if (changes.stream().noneMatch(change -> change.getAmount() != 0L)) {
            throw SingleMessageException.illegalArgument("none.zero.resources.value.required");
        }
        Map<Service, ? extends List<? extends QuotaChangeRequest.ChangeAmount>> changesByService = changes.stream()
                .collect(Collectors.groupingBy(c -> c.getResource().getService()));
        for (Service service : changesByService.keySet()) {
            ServiceChangesValidator validator = RESOURCE_VALIDATOR_BY_SERVICE_KEY.get(service.getKey());
            if (validator != null) {
                validator.validateChanges(context, changesByService.get(service));
            }
        }
        if (context.getCampaign() != null) {
            serviceDictionaryRestrictionManager.checkRequestByDictionary(context, changes);
        }
    }

    private Errors<LocalizableString> validateResourcePreorderReasonType(@Nullable DiResourcePreorderReasonType resourcePreorderReasonType,
                                                                         @Nullable Long goalId) {
        if (resourcePreorderReasonType == null) {
            if (goalId != null) {
                return Errors.of(LocalizableString.of("goal.id.field.must.be.empty"));
            }
            return new Errors<>();
        }
        switch (resourcePreorderReasonType) {
            case GOAL:
                if (goalId == null) {
                    return Errors.of(LocalizableString.of("goal.id.field.required.for.request.with.goal"));
                }
                break;
            case GROWTH:
                if (goalId != null) {
                    return Errors.of(LocalizableString.of("goal.id.field.prohibited.for.growth.request"));
                }
                break;
            case NOTHING:
                return Errors.of(LocalizableString.of("invalid.reason.type"));
        }
        return new Errors<>();
    }

    private boolean isFinalStatus(QuotaChangeRequest.Status status) {
        return READ_ONLY_STATUSES.contains(status);
    }

    private Errors<LocalizableString> validateGoal(Goal goal) {
        Errors<LocalizableString> errors = new Errors<>();
        if (!VALID_STATUS_SET.contains(goal.getStatus())) {
            errors.add(LocalizableString.of("goal.has.invalid.status", goal.getId(), goal.getStatus()));
        }
        if (!VALID_IMPORTANCE_SET.contains(goal.getImportance())) {
            errors.add(LocalizableString.of("goal.has.invalid.importance", goal.getId(), goal.getImportance()));
        }
        return errors;
    }

    private void validateDraftAnswers(DiResourcePreorderReasonType resourcePreorderReasonType, Map<Long, String> data) {
        Set<Long> requiredAnswers = getRequiredAnswers(resourcePreorderReasonType);
        Set<Long> keys = getProvidedAnswerKeys(data);
        validateWrongAnswers(requiredAnswers, keys);
    }

    private void validateWrongAnswers(@NotNull Set<Long> requiredAnswers, @NotNull Set<Long> keys) {
        Sets.SetView<Long> wrongAnswers = Sets.difference(keys, requiredAnswers);
        if (!wrongAnswers.isEmpty()) {
            throw SingleMessageException.illegalArgument("wrong.answers");
        }
    }

    @NotNull
    private Set<Long> getProvidedAnswerKeys(@NotNull Map<Long, String> data) {
        return data.entrySet().stream()
                .filter(e -> StringUtils.isNotEmpty(e.getValue()))
                .map(Map.Entry::getKey)
                .collect(Collectors.toSet());
    }

    @NotNull
    private Set<Long> getRequiredAnswers(DiResourcePreorderReasonType diResourcePreorderReasonType) {
        Set<Long> requiredAnswers;
        switch (diResourcePreorderReasonType) {
            case GOAL:
                requiredAnswers = goalQuestionHelper.getGoalAnswerIds();
                break;
            case GROWTH:
                requiredAnswers = goalQuestionHelper.getGrowthAnswerIds();
                break;
            case NOTHING:
            default:
                throw SingleMessageException.illegalArgument("unexpected.reason.type.value", diResourcePreorderReasonType);
        }

        return requiredAnswers;
    }

    @NotNull
    private QuotaChangeRequest createQuotaRequestTicket(QuotaChangeRequest quotaChangeRequest, RequestContext ctx) {
        String trackerIssueKey = quotaChangeRequestTicketManager.createTicketForQuotaChangeRequest(quotaChangeRequest);
        if (trackerIssueKey == null) {
            return quotaChangeRequest;
        }
        return quotaChangeRequestManager.updateIssueKey(quotaChangeRequest, trackerIssueKey, ctx, false).getValue();
    }

    private void throwErrors(Errors<LocalizableString> errors) {
        if (errors.hasErrors()) {
            throw MultiMessageException.illegalArgument(errors);
        }
    }

    private boolean transitionAllowed(Map<QuotaChangeRequest.Status, Set<QuotaChangeRequest.Status>> allowedTransitions,
                                      QuotaChangeRequest.Status from,
                                      QuotaChangeRequest.Status to) {
        return allowedTransitions.getOrDefault(from, Set.of()).contains(to);
    }

}
