package ru.yandex.direct.api.v5.units.logging;

import java.util.Optional;

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

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.context.units.OperatorBrandData;
import ru.yandex.direct.api.v5.context.units.OperatorClientData;
import ru.yandex.direct.api.v5.context.units.OperatorData;
import ru.yandex.direct.api.v5.context.units.SubclientBrandData;
import ru.yandex.direct.api.v5.context.units.SubclientClientData;
import ru.yandex.direct.api.v5.context.units.SubclientData;
import ru.yandex.direct.api.v5.context.units.UnitsBucket;
import ru.yandex.direct.api.v5.context.units.UnitsContext;
import ru.yandex.direct.api.v5.context.units.UnitsLogData;
import ru.yandex.direct.api.v5.security.DirectApiPreAuthentication;
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.ApiUserService;
import ru.yandex.direct.core.units.api.UnitsBalance;
import ru.yandex.direct.core.units.service.UnitsService;
import ru.yandex.direct.tracing.Trace;

@Component
@ParametersAreNonnullByDefault
public class UnitsLogDataFactory {

    private static final Logger logger = LoggerFactory.getLogger(UnitsLogDataFactory.class);

    private final UnitsService unitsService;
    private final ApiUserService apiUserService;
    private final UnitsBucketFactory unitsBucketFactory;

    @Autowired
    public UnitsLogDataFactory(UnitsService unitsService,
                               ApiUserService apiUserService,
                               UnitsBucketFactory unitsBucketFactory) {
        this.unitsService = unitsService;
        this.apiUserService = apiUserService;
        this.unitsBucketFactory = unitsBucketFactory;
    }

    /**
     * Дополнить {@link UnitsLogData} корзиной держателя баллов.
     * Следует вызывать в случае успешного завершения обработки запроса в сервисе.
     *
     * @param unitsContext контекст баллов текущего запроса,
     *                     содержащий предзаполненную ранее структуру {@link UnitsLogData}.
     */
    public void addUnitsHolderBucket(UnitsContext unitsContext) {
        UnitsLogData unitsLogData = unitsContext.getUnitsLogData();
        UnitsBalance unitsBalance = unitsContext.getUnitsBalance();

        if (unitsBalance == null) {
            logIsNull("unitsBalance");
            return;
        } else if (unitsLogData == null) {
            logIsNull("unitsLogData");
            return;
        }

        unitsLogData
                .withUnitsUsedLogin(getUnitsUsedLogin(unitsContext))
                .withBucket(unitsBucketFactory.createUnitsHolderBucket(unitsLogData, unitsBalance));
    }

    /**
     * Дополнить {@link UnitsLogData} корзиной баллов оператора.
     *
     * @param unitsContext контекст баллов текущего запроса,
     *                     содержащий предзаполненную ранее структуру {@link UnitsLogData}.
     */
    public void addOperatorBucket(UnitsContext unitsContext) {
        UnitsLogData unitsLogData = unitsContext.getUnitsLogData();
        UnitsBalance operatorUnitsBalance = unitsContext.getOperatorUnitsBalance();

        if (operatorUnitsBalance == null) {
            logIsNull("operatorUnitsBalance");
            return;
        } else if (unitsLogData == null) {
            logIsNull("unitsLogData");
            return;
        }

        unitsLogData
                .withIsOperatorFailure(true)
                .withUnitsUsedLogin(getOperator(unitsContext))
                .withBucket(unitsBucketFactory.createOperatorBucket(unitsLogData, operatorUnitsBalance));
    }

    @Nullable
    private String getUnitsUsedLogin(UnitsContext unitsContext) {
        return Optional.ofNullable(unitsContext.getUnitsUsedUser())
                .map(ApiUser::getLogin)
                .orElse(null);
    }

    @Nullable
    private String getOperator(UnitsContext unitsContext) {
        return Optional.ofNullable(unitsContext.getOperator())
                .map(ApiUser::getLogin)
                .orElse(null);
    }

    /**
     * Сформировать структуру {@link UnitsLogData}, содержащую информацию об участниках запроса и их балансах баллов.
     * На данном этапе не заполняется корзина – {@link UnitsBucket} и логин пользователя, с которого списаны баллы –
     * {@code unitsUsedLogin}, поскольку эта информация становится известна лишь после того, как сервис отработал.
     *
     * @param preAuth объект предварительной аутентификации.
     * @return Структура {@link UnitsLogData} со всей доступной информацией на момент начала обработки запроса.
     */
    public UnitsLogData createUnitsLogData(DirectApiPreAuthentication preAuth) {
        return new UnitsLogData()

                .withOperator(buildOperator(preAuth))
                .withSubcilent(buildSubclient(preAuth))

                .withClientLogin(preAuth.getSubjectUser().map(User::getLogin).orElse(null))
                .withRequestId(Trace.current().getSpanId())
                .withUseOperatorUnitsMode(preAuth.getDirectApiCredentials().getUseOperatorUnitsMode());
    }

    private OperatorData buildOperator(DirectApiPreAuthentication preAuth) {
        ApiUser chiefOperator = preAuth.getChiefOperator();
        return new OperatorData()
                .withBrand(buildOperatorBrand(chiefOperator))

                .withOperatorLogin(preAuth.getOperator().getLogin())
                .withOperatorUid(preAuth.getOperator().getUid())

                .withClient(buildOperatorClient(chiefOperator));
    }

    private OperatorBrandData buildOperatorBrand(ApiUser chiefOperator) {
        ApiUser brandChiefOperator = getBrandChief(chiefOperator);
        if (brandChiefOperator == null) {
            return new OperatorBrandData();
        }
        return new OperatorBrandData()
                .withOperatorBrandClientId(brandChiefOperator.getClientId().asLong())
                .withOperatorBrandLogin(brandChiefOperator.getLogin())
                .withOperatorBrandUnitsDaily(getDailyLimit(brandChiefOperator))
                .withOperatorBrandUnitsManual(getManualLimit(brandChiefOperator));
    }

    private OperatorClientData buildOperatorClient(ApiUser chiefOperator) {
        return new OperatorClientData()
                .withOperatorClientId(chiefOperator.getClientId().asLong())
                .withOperatorClientLogin(chiefOperator.getLogin())
                .withOperatorClientRole(chiefOperator.getRole())
                .withOperatorClientUnitsDaily(getDailyLimit(chiefOperator))
                .withOperatorClientUnitsManual(getManualLimit(chiefOperator));
    }

    private SubclientData buildSubclient(DirectApiPreAuthentication preAuth) {
        return preAuth.getChiefSubjectUser().map(
                subclient -> new SubclientData()
                        .withBrand(buildSubclientBrand(subclient))
                        .withClient(new SubclientClientData()
                                .withSubclientClientId(subclient.getClientId().asLong())
                                .withSubclientLogin(subclient.getLogin())
                                .withSubclientUid(subclient.getUid())
                                .withSubclientUnitsDaily(getDailyLimit(subclient))
                                .withSubclientUnitsManual(getManualLimit(subclient)))

        ).orElseGet(SubclientData::new);
    }

    private SubclientBrandData buildSubclientBrand(ApiUser chiefSubclient) {
        ApiUser brandChiefClient = getBrandChief(chiefSubclient);
        if (brandChiefClient == null) {
            return new SubclientBrandData();
        }
        return new SubclientBrandData()
                .withSubclientBrandClientId(brandChiefClient.getClientId().asLong())
                .withSubclientBrandLogin(brandChiefClient.getLogin())
                .withSubclientBrandUnitsDaily(getDailyLimit(brandChiefClient))
                .withSubclientBrandUnitsManual(getManualLimit(brandChiefClient));
    }

    private ApiUser getBrandChief(ApiUser user) {
        return apiUserService.getBrandChiefRepFor(user);
    }

    private Integer getDailyLimit(ApiUser user) {
        return unitsService.getLimit(user);
    }

    private Integer getManualLimit(ApiUser user) {
        return Optional.ofNullable(user.getApiUnitsDaily()).map(Long::intValue).orElse(null);
    }

    private static void logIsNull(String name) {
        logger.error("Could not create units bucket: {} is null.", name);
    }
}
