package ru.yandex.direct.api.v5.entity.clients.service;

import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.yandex.direct.api.v5.clients.UpdateRequest;
import com.yandex.direct.api.v5.clients.UpdateResponse;
import com.yandex.direct.api.v5.general.ClientsActionResult;
import com.yandex.direct.api.v5.generalclients.ClientUpdateItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

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.clients.validation.UpdateRequestValidator;
import ru.yandex.direct.api.v5.security.ApiAuthenticationSource;
import ru.yandex.direct.api.v5.units.ApiUnitsService;
import ru.yandex.direct.api.v5.validation.ApiDefect;
import ru.yandex.direct.api.v5.validation.DefectType;
import ru.yandex.direct.api.v5.validation.ValidationException;
import ru.yandex.direct.core.entity.client.model.Client;
import ru.yandex.direct.core.entity.client.service.ClientService;
import ru.yandex.direct.core.entity.client.service.validation.ClientValidationService;
import ru.yandex.direct.core.entity.user.model.User;
import ru.yandex.direct.core.entity.user.service.UserService;
import ru.yandex.direct.core.entity.user.service.validation.UserValidationService;
import ru.yandex.direct.core.security.AccessDeniedException;
import ru.yandex.direct.core.units.OperationSummary;
import ru.yandex.direct.model.AppliedChanges;
import ru.yandex.direct.rbac.RbacRole;
import ru.yandex.direct.validation.result.Defect;
import ru.yandex.direct.validation.result.DefectInfo;
import ru.yandex.direct.validation.result.PathConverter;
import ru.yandex.direct.validation.result.ValidationResult;

import static ru.yandex.direct.core.entity.user.utils.UserUtil.isClient;


@Component
public class UpdateClientOperation {
    private static final Logger logger = LoggerFactory.getLogger(UpdateClientOperation.class);

    private final ApiAuthenticationSource apiAuthenticationSource;
    private final ApiUnitsService apiUnitsService;
    private final UserService userService;
    private final UserValidationService userValidationService;
    private final ClientService clientService;
    private final ClientValidationService clientValidationService;
    private final ResultConverter resultConverter;
    private final UpdateRequestValidator updateRequestValidator;

    @Autowired
    public UpdateClientOperation(ApiAuthenticationSource apiAuthenticationSource,
                                 ApiUnitsService apiUnitsService,
                                 UserService userService,
                                 UserValidationService userValidationService, ClientService clientService,
                                 ClientValidationService clientValidationService, ResultConverter resultConverter,
                                 UpdateRequestValidator updateRequestValidator) {
        this.apiAuthenticationSource = apiAuthenticationSource;
        this.apiUnitsService = apiUnitsService;
        this.userService = userService;
        this.userValidationService = userValidationService;
        this.clientService = clientService;
        this.clientValidationService = clientValidationService;
        this.resultConverter = resultConverter;
        this.updateRequestValidator = updateRequestValidator;
    }

    public UpdateResponse perform(UpdateRequest request) {
        User targetUser = checkRightsAndGetTargetUser();
        validate(request);

        // В запросе на обновление clients.update всегда одна строка. Это проверяется при валидации запроса.
        ClientUpdateItem updateItem = request.getClients().get(0);
        UserClientChanges changes = UpdateConverter.convertToChanges(
                updateItem, targetUser.getUid(), targetUser.getClientId().asLong());

        // validate changes
        AppliedChanges<User> userAppliedChanges = changes.getUserChanges().applyTo(targetUser);
        ValidationResult<User, Defect> userVr = userValidationService.validate(userAppliedChanges);

        AppliedChanges<Client> clientAppliedChanges = clientService.applyChanges(changes.getClientChanges());
        ValidationResult<Client, Defect> clientVr = clientValidationService.validate(clientAppliedChanges);

        if (userVr.hasAnyErrors() || clientVr.hasAnyErrors()) {
            apiUnitsService.withdraw(OperationSummary.unsuccessful());
            return createErrorResponse(userVr, clientVr);
        } else {
            userService.update(userAppliedChanges);
            clientService.update(clientAppliedChanges);

            // списать баллы за обновление одной записи
            apiUnitsService.withdraw(OperationSummary.successful(1));
            return createSuccessResponse(targetUser.getClientId().asLong());
        }
    }

    private User checkRightsAndGetTargetUser() {
        User operator = apiAuthenticationSource.getOperator();
        User subclient = apiAuthenticationSource.getSubclient();

        if (operator.getRole() == RbacRole.PLACER) {
            throw new AccessDeniedException("Оператор с ролью PLACER не допускается к изменению данных клиента");
        }

        if (operator.getRole() == RbacRole.AGENCY) {
            throw new AccessDeniedException("Представители агентств не допускается к изменению данных клиента");
        }

        if (isClient(operator) && !userIsChief(operator) && !Objects.equals(operator.getUid(), subclient.getUid())) {
            // оператор с ролью "Клиент", не являющийся главным представителем, может получать только свои данные
            throw new AccessDeniedException("У оператора нет прав на указанного пользователя",
                    ApiFaultTranslations.INSTANCE.detailedNoRightsForUser());
        }
        return subclient;
    }

    /**
     * Валидирует запрос; в случае ошибки списывает баллы и выкидывает исключение
     */
    private void validate(UpdateRequest request) {
        ValidationResult<UpdateRequest, DefectType> vr = updateRequestValidator.validate(request);
        if (vr.hasAnyErrors()) {
            logger.debug("can not update clients: request contains errors");
            apiUnitsService.withdraw(OperationSummary.unsuccessful());
            // формат ошибочного ответа предполагает только одну ошибку, поэтому возвращаем первую встретившуюся
            DefectInfo<DefectType> criticalErrorInfo = vr.flattenErrors().get(0);
            throw new ValidationException(new ApiDefect(criticalErrorInfo));
        }
    }

    private UpdateResponse createErrorResponse(ValidationResult<User, Defect> userVr,
                                               ValidationResult<Client, Defect> clientVr) {
        ClientsActionResult actionResult = new ClientsActionResult();

        PathConverter pathConverter = ApiPathConverter.forClientsUpdate();
        List<DefectInfo<DefectType>> errors = Lists.newArrayList(
                Iterables.concat(userVr.flattenErrors(), clientVr.flattenErrors())).stream()
                .map(d -> resultConverter.toDefectInfo(d, null))
                .collect(Collectors.toList());
        resultConverter.addErrors(actionResult, errors, pathConverter);

        List<DefectInfo<DefectType>> warnings = Lists.newArrayList(
                Iterables.concat(userVr.flattenWarnings(), clientVr.flattenWarnings())).stream()
                .map(d -> resultConverter.toDefectInfo(d, null))
                .collect(Collectors.toList());
        resultConverter.addWarnings(actionResult, warnings, pathConverter);
        return new UpdateResponse().withUpdateResults(actionResult);
    }

    private UpdateResponse createSuccessResponse(Long clientId) {
        return new UpdateResponse().withUpdateResults(new ClientsActionResult().withClientId(clientId));
    }

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