package ru.yandex.direct.api.v5.entity.agencyclients.delegate;

import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import com.google.common.collect.ImmutableSet;
import com.yandex.direct.api.v5.agencyclients.UpdateRequest;
import com.yandex.direct.api.v5.agencyclients.UpdateResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import ru.yandex.direct.api.v5.ApiFaultTranslations;
import ru.yandex.direct.api.v5.common.ApiPathConverter;
import ru.yandex.direct.api.v5.converter.ResultConverter;
import ru.yandex.direct.api.v5.entity.BaseApiServiceDelegate;
import ru.yandex.direct.api.v5.entity.agencyclients.service.UpdateAgencyClientConverter;
import ru.yandex.direct.api.v5.entity.agencyclients.service.UserAgencyClientChanges;
import ru.yandex.direct.api.v5.entity.agencyclients.validation.UpdateAgencyClientRequestValidator;
import ru.yandex.direct.api.v5.result.ApiMassResult;
import ru.yandex.direct.api.v5.result.ApiResult;
import ru.yandex.direct.api.v5.security.ApiAuthenticationSource;
import ru.yandex.direct.api.v5.validation.DefectType;
import ru.yandex.direct.core.entity.client.model.Client;
import ru.yandex.direct.core.entity.client.service.ClientUpdateOperation;
import ru.yandex.direct.core.entity.client.service.ClientUpdateOperationProvider;
import ru.yandex.direct.core.entity.grants.model.Grants;
import ru.yandex.direct.core.entity.grants.service.GrantsUpdateOperation;
import ru.yandex.direct.core.entity.grants.service.GrantsUpdateOperationProvider;
import ru.yandex.direct.core.entity.user.model.ApiUser;
import ru.yandex.direct.core.entity.user.model.User;
import ru.yandex.direct.core.entity.user.service.UserUpdateOperation;
import ru.yandex.direct.core.entity.user.service.UserUpdateOperationProvider;
import ru.yandex.direct.core.security.AccessDeniedException;
import ru.yandex.direct.model.ModelChanges;
import ru.yandex.direct.operation.aggregator.PartiallyApplicableOperationAggregator;
import ru.yandex.direct.rbac.RbacRole;
import ru.yandex.direct.validation.result.DefectInfo;
import ru.yandex.direct.validation.result.MappingPathConverter;
import ru.yandex.direct.validation.result.PathConverter;
import ru.yandex.direct.validation.result.ValidationResult;

import static ru.yandex.direct.utils.FunctionalUtils.mapList;
import static ru.yandex.direct.validation.result.PathHelper.field;
import static ru.yandex.direct.validation.result.PathHelper.path;

@Service
public class UpdateAgencyClientDelegate
        extends BaseApiServiceDelegate<UpdateRequest, UpdateResponse, List<UserAgencyClientChanges>, ApiResult<Long>> {
    private static final Set<RbacRole> PROHIBITED_ROLES = ImmutableSet.of(
            RbacRole.SUPERREADER, RbacRole.SUPPORT, RbacRole.MEDIA, RbacRole.PLACER);

    private static final String SUB_PATH_CLIENT = "*client";
    private static final String SUB_PATH_USER = "*user";
    private static final String SUB_PATH_GRANTS = "*grants";

    private static final PathConverter AGENCY_CLIENTS_UPDATE_PATH_CONVERTER = MappingPathConverter
            .builder(ApiPathConverter.class, "AgencyClientsUpdate")
            .ignore(SUB_PATH_CLIENT)
            .ignore(SUB_PATH_USER)
            .ignore(SUB_PATH_GRANTS)
            .add(Client.ID.name(), "ClientId")
            .add(User.EMAIL.name(), "Notification.Email")
            .add(User.PHONE.name(), "Phone")
            .add(User.FIO.name(), "ClientInfo")
            .add(Grants.ALLOW_IMPORT_XLS.name(), "Grants.IMPORT_XLS")
            .add(Grants.ALLOW_EDIT_CAMPAIGN.name(), "Grants.EDIT_CAMPAIGNS")
            .add(Grants.ALLOW_TRANSFER_MONEY.name(), "Grants.TRANSFER_MONEY")
            .build();

    private final UpdateAgencyClientConverter updateAgencyClientConverter;
    private final ResultConverter resultConverter;
    private final UpdateAgencyClientRequestValidator updateRequestValidator;
    private final ClientUpdateOperationProvider clientUpdateOperationProvider;
    private final UserUpdateOperationProvider userUpdateOperationProvider;
    private final GrantsUpdateOperationProvider grantsUpdateOperationProvider;

    @Autowired
    public UpdateAgencyClientDelegate(
            ApiAuthenticationSource auth,
            UpdateAgencyClientConverter updateAgencyClientConverter,
            ResultConverter resultConverter, UpdateAgencyClientRequestValidator updateRequestValidator,
            ClientUpdateOperationProvider clientUpdateOperationProvider,
            UserUpdateOperationProvider userUpdateOperationProvider,
            GrantsUpdateOperationProvider grantsUpdateOperationProvider) {
        super(AGENCY_CLIENTS_UPDATE_PATH_CONVERTER, auth);
        this.updateAgencyClientConverter = updateAgencyClientConverter;
        this.resultConverter = resultConverter;
        this.updateRequestValidator = updateRequestValidator;
        this.grantsUpdateOperationProvider = grantsUpdateOperationProvider;
        this.clientUpdateOperationProvider = clientUpdateOperationProvider;
        this.userUpdateOperationProvider = userUpdateOperationProvider;
    }

    @Override
    public ValidationResult<UpdateRequest, DefectType> validateRequest(UpdateRequest request) {
        return updateRequestValidator.validate(request);
    }

    @Override
    public List<UserAgencyClientChanges> convertRequest(UpdateRequest externalRequest) {
        return updateAgencyClientConverter.convertToChanges(externalRequest);
    }

    @Override
    public ApiMassResult<Long> processRequest(List<UserAgencyClientChanges> internalRequest) {
        // Оператор (тот чей токен указан) должен иметь права на представителя агенства от имени которого выполняется
        // запрос (из Client-Login). Обычно либо представитель этого же агенства, либо менеджер агенства.
        ApiUser operator = auth.getOperator();
        // Обновление происходит от имени того, кто записан в Client-Login. Т.к. это агенстский сервис, то
        // в Client-Login должен быть логин представителя агентства.
        ApiUser agencyRep = auth.getSubclient();
        checkOperatorRights(operator, agencyRep);

        List<ModelChanges<Client>> clientChanges = mapList(internalRequest, UserAgencyClientChanges::getClientChanges);
        ClientUpdateOperation clientUpdateOperation =
                clientUpdateOperationProvider.get(clientChanges, agencyRep.getUid());

        List<ModelChanges<User>> userChanges = mapList(internalRequest, UserAgencyClientChanges::getUserChanges);
        UserUpdateOperation userUpdateOperation = userUpdateOperationProvider.get(agencyRep.getUid(), userChanges);

        ApiUser agencyChiefRep = auth.getChiefSubclient();
        List<ModelChanges<Grants>> grantsChanges = mapList(internalRequest, UserAgencyClientChanges::getGrantsChanges);
        GrantsUpdateOperation grantsUpdateOperation = grantsUpdateOperationProvider.get(
                grantsChanges, agencyRep.getUid(), agencyChiefRep.getClientId());

        PartiallyApplicableOperationAggregator<Long> operationAggregator = PartiallyApplicableOperationAggregator.of(
                SUB_PATH_CLIENT, clientUpdateOperation,
                SUB_PATH_USER, userUpdateOperation,
                SUB_PATH_GRANTS, grantsUpdateOperation);
        return resultConverter.toApiMassResult(operationAggregator.prepareAndApplyPartialTogether());
    }

    @Override
    public UpdateResponse convertResponse(ApiResult<List<ApiResult<Long>>> result) {
        Function<List<DefectInfo<DefectType>>, List<DefectInfo<DefectType>>> filterErrors = (errors) -> errors.stream()
                // В случае, если пользователь API прислал неправильный clientId, то вычисляемое поле user.id тоже будет
                // некорректное но эту ошибку не стоит выдавать пользователю, т.к. значение user.id вычисляемое.
                .filter(di -> !di.getPath().startsWith(path(field(SUB_PATH_USER), field("id"))))
                // поле clientId в grants дублирует clientId из клиента, значит тоже игнорируем,
                // чтобы не дублировались ошибки
                .filter(di -> !di.getPath().startsWith(path(field(SUB_PATH_GRANTS), field("clientId"))))
                .collect(Collectors.toList());

        List<ApiResult<Long>> filteredResults = result.getResult().stream()
                .map(r -> new ApiResult<>(r.getResult(), filterErrors.apply(r.getErrors()),
                        r.getWarnings(), r.getState()))
                .collect(Collectors.toList());

        return new UpdateResponse().withUpdateResults(
                mapList(filteredResults, r -> resultConverter.convertToClientsActionResult(r, apiPathConverter)));
    }

    private void checkOperatorRights(ApiUser operator, ApiUser agencyRep) {
        if (operator.isAgency() && !userIsChief(operator) && !Objects.equals(operator.getUid(), agencyRep.getUid())) {
            // оператор с ролью "Агенство", не являющийся главным представителем, может обновлять данные
            // субклиентов только от своего имени
            throw new AccessDeniedException(ApiFaultTranslations.INSTANCE.detailedNoRightsForUser());
        }
        if (PROHIBITED_ROLES.contains(operator.getRole())) {
            throw new AccessDeniedException(ApiFaultTranslations.INSTANCE.detailedNoRightsForAgencyService());
        }
    }

    private boolean userIsChief(User user) {
        return Objects.equals(user.getUid(), user.getChiefUid());
    }
}
