package ru.yandex.webmaster3.storage.user.service;

import com.google.common.base.Preconditions;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.yandex.ydb.core.StatusCode;
import com.yandex.ydb.core.UnexpectedResultException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import ru.yandex.webmaster3.core.data.WebmasterUser;
import ru.yandex.webmaster3.core.util.RetryUtils;
import ru.yandex.webmaster3.core.worker.client.WorkerClient;
import ru.yandex.webmaster3.core.worker.task.InitializeUserTaskData;
import ru.yandex.webmaster3.storage.user.UserInitializationInfo;
import ru.yandex.webmaster3.storage.user.UserTakeoutDataProvider;
import ru.yandex.webmaster3.storage.user.dao.InitializedUsersYDao;
import ru.yandex.webmaster3.storage.util.ydb.exception.WebmasterYdbException;

import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @author leonidrom
 */
@Service
public class InitializedUsersService implements UserTakeoutDataProvider {
    private static final long CACHE_DURATION_MINUTES = 1;
    private static final Duration RETRY_INITIALIZATION_AFTER = Duration.standardMinutes(1);

    // в кеше хранятся только инициализированные пользователи
    private final Cache<Long, UserInitializationInfo> initializedUsersInfoCache = CacheBuilder.newBuilder()
            .maximumSize(32_000)
            .expireAfterWrite(CACHE_DURATION_MINUTES, TimeUnit.MINUTES)
            .build();

    private final InitializedUsersYDao initializedUsersYDao;
    private final WorkerClient workerClient;

    @Autowired
    public InitializedUsersService(
            InitializedUsersYDao initializedUsersYDao,
            @Qualifier("lbWorkerClient")WorkerClient workerClient) {
        this.initializedUsersYDao = initializedUsersYDao;
        this.workerClient = workerClient;
    }

    @Nullable
    public UserInitializationInfo getUserInfo(long userId) {
        // посмотрим есть ли пользователь в кеше
        var userInfo = initializedUsersInfoCache.getIfPresent(userId);
        if (userInfo != null) {
            Preconditions.checkArgument(userInfo.initialized());
            return userInfo;
        }

        // в кеше пользователя нет, читаем из базы
        userInfo = getUserInfoUncached(userId);
        if (userInfo != null) {
            if (userInfo.initialized()) {
                // сохраним инициализированного пользователя в кеше
                initializedUsersInfoCache.put(userId, userInfo);
            }
        }

        return userInfo;
    }

    @Nullable
    public UUID getUUIDById(Long userId) {
        var userInfo = getUserInfo(userId);
        return userInfo == null? null : userInfo.userUuid();
    }

    @Nullable
    public Long getIdByUUIDUncached(UUID userUUID) {
        return initializedUsersYDao.getIdByUUID(userUUID);
    }

    public void initializeUserIfNeeded(long userId) {
        var userInfo = getUserInfo(userId);
        if (userInfo != null && userInfo.initialized()) {
            return;
        }

        if (userInfo == null || userInfo.lastUpdate().isBefore(DateTime.now().minus(RETRY_INITIALIZATION_AFTER))) {
            updateInitializationInfo(userId, userInfo != null);
            workerClient.enqueueTask(new InitializeUserTaskData(userId, InitializeUserTaskData.EmailInitMode.NONE,
                    null, null, null));
        }
    }

    // Для случая когда полная инициализация с подпиской на рассылки не нужна,
    // а нужно просто завести uuid для этого пользователя
    public void createUUIDRecordIfNeeded(long userId) {
        var userInfo = getUserInfo(userId);
        if (userInfo == null) {
            initializedUsersYDao.markInitializationStart(userId, DateTime.now(), UUID.randomUUID());
        }
    }

    public void markInitialized(long userId) {
        initializedUsersYDao.markInitialized(userId, DateTime.now());
    }

    @Override
    public void deleteUserData(WebmasterUser user) {
        // удаляется в другом месте
    }

    @Override
    public @NotNull List<String> getTakeoutTables() {
        return List.of(
                initializedUsersYDao.getTablePath()
        );
    }

    private UserInitializationInfo getUserInfoUncached(long userId) {
        try {
            return RetryUtils.query(
                    RetryUtils.instantRetry(3),
                    () -> initializedUsersYDao.getUserInfo(userId)
            );
        } catch (Exception e) {
            throw new WebmasterYdbException("Ydb error", e);
        }
    }

    private void updateInitializationInfo(long userId, boolean isRetry) {
        if (isRetry) {
            initializedUsersYDao.markInitializationRetry(userId, DateTime.now());
        } else {
            try {
                initializedUsersYDao.markInitializationStart(userId, DateTime.now(), UUID.randomUUID());
            } catch (UnexpectedResultException e) {
                if (e.getStatusCode() == StatusCode.PRECONDITION_FAILED) {
                    // случился рейс на insert, игнорируем
                    return;
                }

                throw e;
            }
        }
    }
}
