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

import java.util.Arrays;
import java.util.Optional;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletResponse;

import com.typesafe.config.ConfigFactory;
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.ApiContextHolder;
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.api.v5.units.logging.UnitsLogDataFactory;
import ru.yandex.direct.api.v5.units.logging.UnitsLogWriter;
import ru.yandex.direct.core.entity.user.model.ApiUser;
import ru.yandex.direct.core.units.Costs;
import ru.yandex.direct.core.units.OperationCosts;
import ru.yandex.direct.core.units.OperationSummary;
import ru.yandex.direct.core.units.api.UnitsBalance;
import ru.yandex.direct.core.units.decorator.AdjustedUnitsBalanceDecorator;
import ru.yandex.direct.core.units.decorator.FreeOfChargeUnitsBalanceDecorator;
import ru.yandex.direct.core.units.service.UnitsService;
import ru.yandex.direct.core.units.storage.StorageErrorException;

@Component
public class ApiUnitsService {
    public static final String UNITS_COSTS_CONF = "units-costs.conf";

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

    private final Costs costs;
    private final UnitsService unitsService;
    private final ApiContextHolder apiContextHolder;
    private final UnitsLogDataFactory unitsLogDataFactory;
    private final UnitsLogWriter unitsLogWriter;

    @Autowired
    public ApiUnitsService(UnitsService unitsService,
                           ApiContextHolder apiContextHolder,
                           UnitsLogDataFactory unitsLogDataFactory,
                           UnitsLogWriter unitsLogWriter) {
        this.unitsService = unitsService;
        this.apiContextHolder = apiContextHolder;
        this.costs = new Costs(ConfigFactory.load(UNITS_COSTS_CONF));
        this.unitsLogDataFactory = unitsLogDataFactory;
        this.unitsLogWriter = unitsLogWriter;
    }

    /**
     * @param service service name
     * @param method  method name
     * @return {@link OperationCosts} for given pair ({@code service}, {@code method}
     */
    public OperationCosts getCostsForOperation(String service, String method) {
        return costs.getOperationCosts(service, method);
    }

    /**
     * This method should be used when user is authenticated.
     *
     * @return {@link UnitsBalance} for current user
     */
    @Nonnull
    public UnitsBalance getAdjustedUnitsBalance(
            @Nonnull DirectApiPreAuthentication preAuth,
            @Nonnull ApiUser unitsHolder) {
        return getUnitsInternal(preAuth, unitsHolder);
    }

    /**
     * Снять баллы за ошибку уровня запроса с учетом заданного кода ошибки.
     * Если передан {@code null}, считается, что произошла неизвестная ошибка,
     * списывается некоторое дефолтное количество баллов.
     */
    public void withdrawForRequestError(@Nullable Integer errorCode) {
        UnitsContext unitsContext = getUnitsContext();
        UnitsBalance operatorUnitsBalance = unitsContext.getOperatorUnitsBalance();
        if (operatorUnitsBalance != null) {
            operatorUnitsBalance.withdraw(costs.getServiceErrorCost(errorCode));
            unitsLogDataFactory.addOperatorBucket(unitsContext);
            writeUnitsLog(unitsContext.getUnitsLogData());
            fillApiLogRecordWithUnitsInfo(operatorUnitsBalance);
        }
    }

    /**
     * Снять баллы за операцию
     */
    public void withdraw(OperationSummary operationSummary) {
        OperationCosts operationCosts = apiContextHolder.get().getOperationCosts();
        UnitsContext unitsContext = getUnitsContext();
        UnitsBalance unitsBalance = unitsContext.getUnitsBalance();
        if (operationCosts != null && unitsBalance != null) {
            int requestCost = operationCosts.calcRequestCost(operationSummary);
            unitsBalance.withdraw(requestCost);
            unitsLogDataFactory.addUnitsHolderBucket(unitsContext);
            writeUnitsLog(unitsContext.getUnitsLogData());
            fillApiLogRecordWithUnitsInfo(unitsBalance);
        } else {
            logger.warn("Skip units withdrawing because required parameter(s) are absent in ApiContext."
                    + " unitsBalance: {}, operationCosts: {}", unitsBalance, operationCosts);
        }
    }

    private void writeUnitsLog(UnitsLogData unitsLogData) {
        unitsLogWriter.write(unitsLogData);
    }

    /**
     * Проставляет в http-ответ заголовок {@code Units} с потраченными баллами
     * и заголовок {@code Units-Used-Login} с логином пользователя, чьи баллы использовались.
     *
     * @param response ответ, куда будут записаны заголовки
     */
    public void setUnitsHeaders(HttpServletResponse response) {
        UnitsBalance unitsBalance = getUnitsContext().getUnitsBalance();
        ApiUser unitsUsedUser = getUnitsContext().getUnitsUsedUser();
        ApiUser operator = getUnitsContext().getOperator();
        UnitsBalance operatorUnitsBalance = getUnitsContext().getOperatorUnitsBalance();

        if (operator == null || unitsBalance == null || unitsUsedUser == null || operatorUnitsBalance == null) {
            logger.warn("None of units headers have been set to the response."
                            + " unitsBalance = {}; unitsUsedUser = {}; operatorUnitsBalance = {}; operator = {}.",
                    unitsBalance, unitsUsedUser, operatorUnitsBalance, operator);
            return;
        }
        logger.trace("Set units headers for current response");
        // в случае возникновения ошибки запроса баллы списываются с оператора, поэтому, при таком списании,
        // возвращаем баланс баллов оператора
        response.setHeader("Units", getUnitsResponseString(
                operatorUnitsBalance.spentInCurrentRequest() > 0 ? operatorUnitsBalance : unitsBalance));
        response.setHeader("Units-Used-Login", operatorUnitsBalance.spentInCurrentRequest() > 0 ?
                operator.getLogin() : unitsUsedUser.getLogin());
    }

    @Nonnull
    private UnitsBalance getCurrentUnitsBalanceFor(ApiUser unitsHolder) {
        try {
            return unitsService.getUnitsBalance(unitsHolder);
        } catch (StorageErrorException e) {
            logger.error("Can't get units balance for clientId {}", unitsHolder.getClientId().asLong(), e);
            return unitsService.createFallbackUnitsBalance(unitsHolder);
        }
    }

    @Nonnull
    private UnitsBalance getUnitsInternal(DirectApiPreAuthentication preAuth, ApiUser unitsHolder) {
        UnitsBalance unitsBalance = getCurrentUnitsBalanceFor(unitsHolder);

        double applicationCoef = costs.getApplicationCoef(preAuth.getApplicationId());
        unitsBalance = new AdjustedUnitsBalanceDecorator(unitsBalance, applicationCoef);

        if (preAuth.getOperator().getRole().isInternal()) {
            logger.debug("Current operator is internal. Its operations are free of charge");
            unitsBalance = new FreeOfChargeUnitsBalanceDecorator(unitsBalance);
        }

        return unitsBalance;
    }

    /**
     * @return String with 'spent/available/total' format
     */
    private String getUnitsResponseString(UnitsBalance unitsBalance) {
        int unitsSpent = unitsBalance.spentInCurrentRequest();
        int availableLimit = unitsBalance.balance();
        int slidingWindowLimit = unitsBalance.getLimit();
        return String.format("%d/%d/%d", unitsSpent, availableLimit, slidingWindowLimit);
    }

    /**
     * Обновляет состояние баланса, с учетом того сколько было потрачено в рамках выполнения текущего запроса
     */
    public boolean updateSpent(UnitsBalance unitsBalance) {
        try {
            unitsService.updateSpent(unitsBalance);
            return true;
        } catch (RuntimeException e) {
            logger.error("Can't update spent units for clientId {}", unitsBalance.getClientId(), e);
            return false;
        }
    }

    private UnitsContext getUnitsContext() {
        return apiContextHolder.get().getUnitsContext();
    }

    private void fillApiLogRecordWithUnitsInfo(UnitsBalance unitsBalance) {
        apiContextHolder.get().getApiLogRecord()
                .withUnitsStats(Arrays.asList(unitsBalance.spentInCurrentRequest(), unitsBalance.balance(),
                        unitsBalance.getLimit()))
                .withUnitsSpendingUserClientId(Optional.ofNullable(getUnitsContext().getUnitsLogData())
                        .map(UnitsLogData::getBucket)
                        .map(UnitsBucket::getBucketClientId)
                        .orElse(0L));
    }

}
