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

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;

import com.google.common.collect.ImmutableSet;
import com.yandex.direct.api.v5.clients.ClientFieldEnum;
import com.yandex.direct.api.v5.clients.GetRequest;
import com.yandex.direct.api.v5.clients.GetResponse;
import com.yandex.direct.api.v5.generalclients.ClientGetItem;
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.context.ApiContextHolder;
import ru.yandex.direct.api.v5.entity.BaseApiServiceDelegate;
import ru.yandex.direct.api.v5.entity.agencyclients.service.RequestedField;
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.api.v5.validation.DefectTypes;
import ru.yandex.direct.core.entity.user.model.User;
import ru.yandex.direct.core.security.AccessDeniedException;
import ru.yandex.direct.core.units.api.UnitsBalance;
import ru.yandex.direct.dbutil.model.ClientId;
import ru.yandex.direct.validation.builder.Constraint;
import ru.yandex.direct.validation.builder.ItemValidationBuilder;
import ru.yandex.direct.validation.builder.When;
import ru.yandex.direct.validation.result.MappingPathConverter;
import ru.yandex.direct.validation.result.ValidationResult;

import static com.yandex.direct.api.v5.clients.ClientFieldEnum.ACCOUNT_QUALITY;
import static com.yandex.direct.api.v5.clients.ClientFieldEnum.CURRENCY;
import static com.yandex.direct.api.v5.clients.ClientFieldEnum.GRANTS;
import static com.yandex.direct.api.v5.clients.ClientFieldEnum.MANAGED_LOGINS;
import static com.yandex.direct.api.v5.clients.ClientFieldEnum.OVERDRAFT_SUM_AVAILABLE;
import static com.yandex.direct.api.v5.clients.ClientFieldEnum.RESTRICTIONS;
import static com.yandex.direct.api.v5.clients.ClientFieldEnum.SETTINGS;
import static ru.yandex.direct.validation.Predicates.notInSet;
import static ru.yandex.direct.validation.builder.Constraint.fromPredicate;

@Component
@ParametersAreNonnullByDefault
public class GetClientsDelegate extends BaseApiServiceDelegate<GetRequest, GetResponse, Set<RequestedField>, ClientGetItem> {
    private static final Logger logger = LoggerFactory.getLogger(GetClientsDelegate.class);

    private static final Set<ClientFieldEnum> IGNORED_AGENCY_FIELDS = ImmutableSet
            .of(CURRENCY, GRANTS, OVERDRAFT_SUM_AVAILABLE, RESTRICTIONS, SETTINGS, ACCOUNT_QUALITY, MANAGED_LOGINS);
    private static final Predicate<List<ClientFieldEnum>> CONTAINS_ANY_EXCEPT_IGNORED = fields -> fields.stream()
            .filter(notInSet(IGNORED_AGENCY_FIELDS)).anyMatch(v -> true);

    private final ApiContextHolder apiContextHolder;
    private final ClientDataFetcher clientDataFetcher;

    @Autowired
    public GetClientsDelegate(ApiAuthenticationSource auth,
                              ApiContextHolder apiContextHolder, ClientDataFetcher clientDataFetcher) {
        super(MappingPathConverter.builder(GetClientsDelegate.class, "emptyConverter").build(), auth);
        this.apiContextHolder = apiContextHolder;
        this.clientDataFetcher = clientDataFetcher;
    }

    @Nullable
    @Override
    public ValidationResult<GetRequest, DefectType> validateRequest(GetRequest externalRequest) {
        Constraint<List<ClientFieldEnum>, DefectType> containsAnyExceptIgnored =
                fromPredicate(CONTAINS_ANY_EXCEPT_IGNORED, DefectTypes.invalidRequestAgencyForbiddenFieldsOnly());
        ItemValidationBuilder<GetRequest, DefectType> vb = ItemValidationBuilder.of(externalRequest);
        vb.item(externalRequest.getFieldNames(), "FieldNames")
                .check(containsAnyExceptIgnored, When.isTrue(auth.getSubclient().isAgency()));
        return vb.getResult();
    }

    @Override
    public Set<RequestedField> convertRequest(GetRequest request) {
        // Для агентств некоторые поля выдавать не нужно, даже если они указаны в запросе
        //
        Predicate<ClientFieldEnum> suitableFields = auth.getSubclient().isAgency()
                ? notInSet(IGNORED_AGENCY_FIELDS)
                : v -> true;
        return request.getFieldNames().stream()
                .filter(suitableFields)
                .map(this::convertFieldName)
                .collect(Collectors.toSet());
    }

    @Override
    public ApiResult<List<ClientGetItem>> processRequest(Set<RequestedField> fieldNames) {
        User operator = auth.getOperator();
        User subclient = auth.getSubclient();

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

        Integer unitsLimit = Optional.ofNullable(apiContextHolder.get().getUnitsContext().getUnitsBalance())
                .map(UnitsBalance::getLimit)
                .orElse(null);
        Map<ClientId, Integer> clientUnitsLimits =
                Collections.singletonMap(subclient.getClientId(), unitsLimit);

        logger.debug("getting main data");
        User chiefSubclient = auth.getChiefSubclient();
        List<ClientGetItem> clientItems = clientDataFetcher.getData(
                fieldNames, subclient.getUid(), operator, chiefSubclient, subclient, clientUnitsLimits);

        return ApiResult.successful(clientItems);
    }

    @Override
    public GetResponse convertResponse(ApiResult<List<ClientGetItem>> result) {
        GetResponse response = new GetResponse();

        List<ClientGetItem> items = result.getResult();
        if (!items.isEmpty()) {
            response.withClients(items);
        }
        return response;
    }


    @SuppressWarnings("Duplicates")
    private RequestedField convertFieldName(ClientFieldEnum fieldName) {
        switch (fieldName) {
            case ACCOUNT_QUALITY:
                return RequestedField.ACCOUNT_QUALITY;
            case ARCHIVED:
                return RequestedField.ARCHIVED;
            case CLIENT_ID:
                return RequestedField.CLIENT_ID;
            case CLIENT_INFO:
                return RequestedField.CLIENT_INFO;
            case COUNTRY_ID:
                return RequestedField.COUNTRY_ID;
            case CREATED_AT:
                return RequestedField.CREATED_AT;
            case CURRENCY:
                return RequestedField.CURRENCY;
            case GRANTS:
                return RequestedField.GRANTS;
            case BONUSES:
                return RequestedField.BONUSES;
            case LOGIN:
                return RequestedField.LOGIN;
            case NOTIFICATION:
                return RequestedField.NOTIFICATION;
            case OVERDRAFT_SUM_AVAILABLE:
                return RequestedField.OVERDRAFT_SUM_AVAILABLE;
            case PHONE:
                return RequestedField.PHONE;
            case REPRESENTATIVES:
                return RequestedField.REPRESENTATIVES;
            case RESTRICTIONS:
                return RequestedField.RESTRICTIONS;
            case SETTINGS:
                return RequestedField.SETTINGS;
            case TYPE:
                return RequestedField.TYPE;
            case SUBTYPE:
                return RequestedField.SUBTYPE;
            case VAT_RATE:
                return RequestedField.VAT_RATE;
            case MANAGED_LOGINS:
                return RequestedField.MANAGED_LOGINS;
            default:
                throw new IllegalStateException("unknown field");
        }
    }

    private boolean isRepsForSameClient(User operator, User subclient) {
        return Objects.equals(operator.getClientId().asLong(), subclient.getClientId().asLong());
    }

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