package ru.yandex.qe.dispenser.ws;

import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.PATCH;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import io.swagger.annotations.Api;
import io.swagger.annotations.Authorization;
import org.jetbrains.annotations.NotNull;
import org.springframework.web.bind.annotation.RequestBody;

import ru.yandex.qe.dispenser.api.v1.DiQuotaChangeRequest;
import ru.yandex.qe.dispenser.api.v1.DiResourcePreOrderRequest;
import ru.yandex.qe.dispenser.api.v1.DiSetAmountResult;
import ru.yandex.qe.dispenser.api.v1.ExpandQuotaChangeRequest;
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.response.DiListResponse;
import ru.yandex.qe.dispenser.api.v1.response.DiResponse;
import ru.yandex.qe.dispenser.domain.Campaign;
import ru.yandex.qe.dispenser.domain.Person;
import ru.yandex.qe.dispenser.domain.QuotaChangeRequest;
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.dao.quota.request.QuotaChangeRequestDao;
import ru.yandex.qe.dispenser.domain.exception.SingleMessageException;
import ru.yandex.qe.dispenser.domain.hierarchy.Session;
import ru.yandex.qe.dispenser.domain.resources_model.QuotaRequestDeliveryContext;
import ru.yandex.qe.dispenser.swagger.DispenserSecurityDefinition;
import ru.yandex.qe.dispenser.swagger.SwaggerTags;
import ru.yandex.qe.dispenser.ws.api.impl.AllocationDestinationsManager;
import ru.yandex.qe.dispenser.ws.api.impl.AllocationStatusManager;
import ru.yandex.qe.dispenser.ws.api.impl.QuotaDistributionManager;
import ru.yandex.qe.dispenser.ws.api.impl.QuotaDistributionValidationManager;
import ru.yandex.qe.dispenser.ws.api.model.distribution.QuotaDistributionPlan;
import ru.yandex.qe.dispenser.ws.bot.ServersCostHelper;
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;
import ru.yandex.qe.dispenser.ws.common.impl.ResultToResponse;
import ru.yandex.qe.dispenser.ws.quota.request.QuotaChangeRequestManager;
import ru.yandex.qe.dispenser.ws.quota.request.SetResourceAmountBody;
import ru.yandex.qe.dispenser.ws.quota.request.workflow.ResourceWorkflow;
import ru.yandex.qe.dispenser.ws.quota.request.workflow.context.PerformerContext;
import ru.yandex.qe.dispenser.ws.reqbody.AllocationDestinationSelectionRequest;
import ru.yandex.qe.dispenser.ws.reqbody.AllocationDestinationSelectionResponse;
import ru.yandex.qe.dispenser.ws.reqbody.DeliveryStatusRequest;
import ru.yandex.qe.dispenser.ws.reqbody.DeliveryStatusResponse;
import ru.yandex.qe.dispenser.ws.reqbody.DistributeQuotaBody;
import ru.yandex.qe.dispenser.ws.reqbody.QuotaRequestPendingDeliveryResponse;
import ru.yandex.qe.dispenser.ws.reqbody.RequestQuotaAllocationBody;
import ru.yandex.qe.dispenser.ws.reqbody.SetResourceAmountBodyOptional;

import static ru.yandex.qe.dispenser.ws.QuotaChangeRequestService.REQUEST_ID;
import static ru.yandex.qe.dispenser.ws.QuotaChangeRequestService.STATUS;

@ParametersAreNonnullByDefault
@Path("/v1/resource-preorder")
@Produces(ServiceBase.APPLICATION_JSON_UTF_8)
@Api(tags = {SwaggerTags.DISPENSER_API}, authorizations = {@Authorization(value = DispenserSecurityDefinition.AUTHORIZATION_SCHEME_NAME)})
@org.springframework.stereotype.Service("resource-preorder")
public class ResourcePreorderRequestService {

    public static final String EXPAND = "expand";
    public static final String CAMPAIGN = "campaign";
    public static final String SUPPRESS_SUMMON = "suppressSummon";

    @Inject
    private QuotaChangeRequestDao requestDao;
    @Inject
    private ResourcePreorderRequestUtils resourcePreorderRequestUtils;
    @Inject
    private ResourceRequestAllocationManager allocationManager;
    @Inject
    private QuotaRequestSerializer requestSerializer;
    @Inject
    private QuotaChangeRequestManager quotaChangeRequestManager;
    @Inject
    private QuotaDistributionValidationManager quotaDistributionValidationManager;
    @Inject
    private QuotaDistributionManager quotaDistributionManager;
    @Inject
    private ResourcePreorderChangeManager changeManager;
    @Inject
    private ServersCostHelper serversCostHelper;
    @Inject
    private QuotaChangeRequestServiceHelper quotaChangeRequestServiceHelper;
    @Inject
    private AllocationDestinationsManager allocationDestinationsManager;
    @Inject
    private AllocationStatusManager allocationStatusManager;
    @Inject
    private QuotaRequestObservabilityService quotaRequestObservabilityService;

    @Access
    @PATCH
    @Path("quotaState")
    public DiSetAmountResult setResourceQuotaState(@QueryParam(SUPPRESS_SUMMON) @DefaultValue("false") boolean suppressSummon,
                                                   @RequestBody final SetResourceAmountBody body) {
        final PerformerContext context = new PerformerContext(Session.WHOAMI.get());

        return setResourceQuotaStateInContext(body, context, suppressSummon);
    }

    @NotNull
    public DiSetAmountResult setResourceQuotaStateInContext(final SetResourceAmountBody body,
                                                            final PerformerContext context,
                                                            final boolean suppressSummon) {
        final QuotaRequestChangeValues changeValues = resourcePreorderRequestUtils.getChanges(body, context);

        changeManager.updateRequestChanges(context, changeValues, suppressSummon);

        return DiSetAmountResult.SUCCESS;
    }

    @Access
    @PATCH
    @Path("quotaStateOptional")
    public DiSetAmountResult setResourceQuotaStateO(@QueryParam(SUPPRESS_SUMMON) @DefaultValue("false") boolean suppressSummon,
                                                    @RequestBody final SetResourceAmountBodyOptional body) {

        final SetResourceAmountBody fullBody = resourcePreorderRequestUtils.getFullBody(body);

        return setResourceQuotaState(suppressSummon, fullBody);
    }

    @Access
    @POST
    @Path("/{requestId}/request-allocation")
    public DiQuotaChangeRequest requestQuotaAllocation(@PathParam("requestId") final QuotaChangeRequest request,
                                                       @QueryParam(SUPPRESS_SUMMON) @DefaultValue("false") boolean suppressSummon,
                                                       @QueryParam(EXPAND) Set<ExpandQuotaChangeRequest> expand) {
        final Person performer = Session.WHOAMI.get();

        if (request.getType() != QuotaChangeRequest.Type.RESOURCE_PREORDER) {
            throw SingleMessageException.illegalArgument("only.resource.preorder.request.can.be.allocated");
        }

        try {
            allocationManager.onManualAllocationRequest(request, performer, suppressSummon);
        } catch (RuntimeException e) {
            allocationFail(request, e);
        }

        Set<BaseResourceChange> baseResourceChanges = quotaChangeRequestManager
                .getBaseResourceChanges(request.getId(), expand);
        return requestSerializer.toView(requestDao.read(request.getId()), performer, expand, baseResourceChanges);
    }

    @Access
    @POST
    @Path("/{requestId}/provider-allocation/{serviceKey}")
    public DiQuotaChangeRequest requestQuotaAllocateToProvider(@PathParam("requestId") final QuotaChangeRequest request,
                                                               @PathParam("serviceKey") final Service service,
                                                               @QueryParam(SUPPRESS_SUMMON) @DefaultValue("false") boolean suppressSummon,
                                                               @QueryParam(EXPAND) Set<ExpandQuotaChangeRequest> expand) {
        final Person person = Session.WHOAMI.get();

        try {
            allocationManager.onManualAllocationRequestService(request, service, person, suppressSummon);
        } catch (RuntimeException e) {
            allocationFail(request, e);
        }

        Set<BaseResourceChange> baseResourceChanges = quotaChangeRequestManager
                .getBaseResourceChanges(request.getId(), expand);
        return requestSerializer.toView(requestDao.read(request.getId()), person, expand, baseResourceChanges);
    }

    @Access
    @POST
    @Path("/{requestId}/provider-allocation/{serviceKey}/_dry-run")
    public QuotaRequestDeliveryContext.View dryRunAllocation(@PathParam("requestId") final QuotaChangeRequest request,
                                                             @PathParam("serviceKey") final Service provider,
                                                             @QueryParam("all") @DefaultValue("false") final boolean allocateAllResources) {
        final Person person = Session.WHOAMI.get();
        return allocationManager.dryRunAllocation(request, provider, person, allocateAllResources).toView();
    }

    @Access
    @POST
    @Idempotent
    @Path("/distribute-quota")
    @Consumes(MediaType.APPLICATION_JSON)
    public Response distributeQuota(@QueryParam(SUPPRESS_SUMMON) @DefaultValue("false") boolean suppressSummon,
                                    @RequestBody final DistributeQuotaBody request) {
        final Person currentUser = Session.WHOAMI.get();
        final PerformerContext performerContext = new PerformerContext(currentUser);
        final Result<DiResponse, ErrorCollection<String, TypedError<String>>> result
                = quotaDistributionValidationManager.validateDistributionParams(request, performerContext)
                .andThen(params -> quotaDistributionManager.planAndLockDistribution(params))
                .apply(distribution -> {
                    quotaDistributionManager.distributeQuota(distribution, performerContext, suppressSummon);
                    return new DiResponse(Response.Status.OK.getStatusCode());
                });
        return ResultToResponse.toResponse(result, false);
    }

    @Access
    @GET
    @Path("/quota-distribution-plan")
    @Consumes(MediaType.APPLICATION_JSON)
    public Response getQuotaDistributionPlan(@RequestBody final DistributeQuotaBody request) {
        final Person currentUser = Session.WHOAMI.get();
        final PerformerContext performerContext = new PerformerContext(currentUser);
        final Result<QuotaDistributionPlan, ErrorCollection<String, TypedError<String>>> result
                = quotaDistributionValidationManager.validateDistributionParams(request, performerContext)
                .andThen(params -> quotaDistributionManager.planDistribution(params).apply(distribution -> {
                    return quotaDistributionManager.showDistributionPlan(distribution, params);
                }));
        return ResultToResponse.toResponse(result);
    }

    @Access
    @GET
    @Path("{requestId}")
    public DiResourcePreOrderRequest getRequest(@PathParam("requestId") @NotNull final QuotaChangeRequest request,
                                                @QueryParam(EXPAND) Set<ExpandQuotaChangeRequest> expand) {
        ServiceEndpointUtils.memoizeServiceForChangeRequest(request);
        final Person performer = Session.WHOAMI.get();
        Set<BaseResourceChange> baseResourceChanges = quotaChangeRequestManager
                .getBaseResourceChanges(request.getId(), expand);
        return getResourcePreOrderView(request, performer, expand, baseResourceChanges);
    }

    private DiResourcePreOrderRequest getResourcePreOrderView(final @NotNull QuotaChangeRequest request, final Person performer,
                                                              Set<ExpandQuotaChangeRequest> expand,
                                                              Set<BaseResourceChange> baseChanges) {
        final DiQuotaChangeRequest requestView = requestSerializer.toView(request, performer, expand, baseChanges);

        final Map<Service, Double> costByService;
        if (ResourceWorkflow.canUserViewBotMoney(performer)) {
            costByService = serversCostHelper.getProviderServersCost(request);
        } else {
            costByService = Collections.emptyMap();
        }

        final List<DiResourcePreOrderRequest.ProviderServersCost> serversCosts = costByService.entrySet().stream()
                .map(e -> new DiResourcePreOrderRequest.ProviderServersCost(e.getKey().getKey(), e.getValue()))
                .collect(Collectors.toList());

        return new DiResourcePreOrderRequest(requestView, serversCosts);
    }

    @NotNull
    @POST
    @Access
    public DiListResponse<DiResourcePreOrderRequest> createRequests(@QueryParam(EXPAND) Set<ExpandQuotaChangeRequest> expand,
                                                                    @QueryParam(CAMPAIGN) Long campaignId,
                                                                    @QueryParam(SUPPRESS_SUMMON) @DefaultValue("false") boolean suppressSummon,
                                                                    @NotNull final Body body) {
        final Person person = Session.WHOAMI.get();
        if (body.getType() != DiQuotaChangeRequest.Type.RESOURCE_PREORDER) {
            throw new IllegalArgumentException("Unsupported request type");
        }
        final Campaign campaign = quotaChangeRequestServiceHelper.tryLoadCampaignWithDefault(campaignId);
        final WithBaseResourceChanges<List<QuotaChangeRequest>> requests = quotaChangeRequestServiceHelper
                .createRequests(body, person, campaign);
        return new DiListResponse<>(
                requests.getValue().stream().map(r -> getResourcePreOrderView(r, person, expand,
                                requests.getChangesByQuotaRequest().getOrDefault(r.getId(), Set.of())))
                        .collect(Collectors.toList())
        );
    }

    @Access
    @PUT
    @Path("{" + REQUEST_ID + "}/status/{" + STATUS + "}")
    public DiResourcePreOrderRequest setStatus(@PathParam(REQUEST_ID) @NotNull final QuotaChangeRequest request,
                                               @PathParam(STATUS) @NotNull final DiQuotaChangeRequest.Status diStatus,
                                               @QueryParam(SUPPRESS_SUMMON) @DefaultValue("false") boolean suppressSummon,
                                               @QueryParam(EXPAND) Set<ExpandQuotaChangeRequest> expand) {
        final Person person = Session.WHOAMI.get();
        final QuotaChangeRequest modifiedRequest = quotaChangeRequestServiceHelper.setStatus(request, diStatus, person,
                suppressSummon);
        Set<BaseResourceChange> baseResourceChanges = quotaChangeRequestManager
                .getBaseResourceChanges(request.getId(), expand);
        return getResourcePreOrderView(modifiedRequest, person, expand, baseResourceChanges);
    }

    @Access
    @POST
    @Path("{" + REQUEST_ID + "}/reset-allocation-note")
    public Response resetShowAllocationNote(@PathParam(REQUEST_ID) @NotNull final QuotaChangeRequest request) {
        final Person person = Session.WHOAMI.get();
        if (ResourceRequestAllocationManager.canUserAllocateQuota(request, person)) {
            quotaChangeRequestManager.setShowAllocationNote(List.of(request.getId()), false);
            return Response.ok().build();
        } else {
            return Response.status(Response.Status.FORBIDDEN).build();
        }
    }

    @Access
    @PATCH
    @Path("{" + REQUEST_ID + "}")
    public DiResourcePreOrderRequest updateRequest(@PathParam(REQUEST_ID) @NotNull final QuotaChangeRequest request,
                                                   @QueryParam(EXPAND) Set<ExpandQuotaChangeRequest> expand,
                                                   @QueryParam(SUPPRESS_SUMMON) @DefaultValue("false") boolean suppressSummon,
                                                   @NotNull final BodyUpdate body) {
        final Person person = Session.WHOAMI.get();
        final WithBaseResourceChanges<QuotaChangeRequest> updatedRequest = quotaChangeRequestServiceHelper
                .updateRequest(request, body, person, suppressSummon);
        return getResourcePreOrderView(updatedRequest.getValue(), person, expand,
                updatedRequest.getChangesByQuotaRequest().getOrDefault(updatedRequest.getValue().getId(), Set.of()));
    }

    @Access
    @POST
    @Path("/find-destination-selection")
    @Consumes(MediaType.APPLICATION_JSON)
    public Response findDestinationSelection(
            @RequestBody AllocationDestinationSelectionRequest request) {
        Person currentUser = Session.WHOAMI.get();
        PerformerContext performerContext = new PerformerContext(currentUser);
        Locale locale = Session.USER_LOCALE.get();
        Result<AllocationDestinationSelectionResponse, ErrorCollection<String, TypedError<String>>> result
                = allocationDestinationsManager.find(request, performerContext, locale);
        return ResultToResponse.toResponse(result);
    }

    @Access
    @POST
    @Path("/find-delivery-status")
    @Consumes(MediaType.APPLICATION_JSON)
    public Response getDeliveryStatuses(@RequestBody DeliveryStatusRequest request) {
        Person currentUser = Session.WHOAMI.get();
        PerformerContext performerContext = new PerformerContext(currentUser);
        Locale locale = Session.USER_LOCALE.get();
        Result<DeliveryStatusResponse, ErrorCollection<String, TypedError<String>>> result
                = allocationStatusManager.getDeliveryStatuses(request, performerContext, locale);
        return ResultToResponse.toResponse(result);
    }

    @Access
    @POST
    @Path("/find-pending-deliveries")
    public Response getPendingDeliveries(@QueryParam("limit") @DefaultValue("100") long limit,
                                         @QueryParam("from") @Nullable String from) {
        Person currentUser = Session.WHOAMI.get();
        PerformerContext performerContext = new PerformerContext(currentUser);
        Locale locale = Session.USER_LOCALE.get();
        Result<QuotaRequestPendingDeliveryResponse, ErrorCollection<String, TypedError<String>>> result
                = allocationStatusManager.getPendingDeliveries(limit, from, performerContext, locale);
        return ResultToResponse.toResponse(result);
    }

    @Access
    @POST
    @Path("/{requestId}/provide-to-account/{serviceKey}")
    public DiQuotaChangeRequest requestQuotaProvideToAccount(
            @PathParam("requestId") final QuotaChangeRequest request,
            @PathParam("serviceKey") final Service service,
            @QueryParam(SUPPRESS_SUMMON) @DefaultValue("false") boolean suppressSummon,
            @QueryParam(EXPAND) Set<ExpandQuotaChangeRequest> expand,
            @RequestBody RequestQuotaAllocationBody body
    ) {
        final Person person = Session.WHOAMI.get();

        try {
            allocationManager.onManualAllocationRequestServiceAccount(request, service, person, suppressSummon, body);
        } catch (RuntimeException e) {
            allocationFail(request, e);
        }

        Set<BaseResourceChange> baseResourceChanges = quotaChangeRequestManager
                .getBaseResourceChanges(request.getId(), expand);
        return requestSerializer.toView(requestDao.read(request.getId()), person, expand, baseResourceChanges);
    }

    private void allocationFail(QuotaChangeRequest request, RuntimeException e) {
        quotaRequestObservabilityService.allocationFail(request, e);
        throw e;
    }
}
