/*
 * Copyright 2024 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.keycloak.models.sessions.infinispan.changes.remote.updater.loginfailures;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;

import org.keycloak.models.RealmModel;
import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.sessions.infinispan.changes.remote.updater.BaseUpdater;
import org.keycloak.models.sessions.infinispan.changes.remote.updater.Expiration;
import org.keycloak.models.sessions.infinispan.changes.remote.updater.Updater;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
import org.keycloak.utils.KeycloakSessionUtil;

/**
 * Implementation of {@link Updater} and {@link UserLoginFailureModel}.
 * <p>
 * It keeps track of the changes made to the entity {@link LoginFailureEntity} and replays on commit.
 */
public class LoginFailuresUpdater extends BaseUpdater<LoginFailureKey, LoginFailureEntity> implements UserLoginFailureModel {

    private final List<Consumer<LoginFailureEntity>> changes;

    private LoginFailuresUpdater(LoginFailureKey key, LoginFailureEntity entity, long version, UpdaterState initialState) {
        super(key, entity, version, initialState);
        if (entity == null) {
            assert initialState == UpdaterState.DELETED;
            changes = List.of();
            return;
        }
        changes = new ArrayList<>(4);
    }

    public static LoginFailuresUpdater create(LoginFailureKey key, LoginFailureEntity entity) {
        return new LoginFailuresUpdater(key, Objects.requireNonNull(entity), NO_VERSION, UpdaterState.CREATED);
    }

    public static LoginFailuresUpdater wrap(LoginFailureKey key, LoginFailureEntity value, long version) {
        return new LoginFailuresUpdater(key, Objects.requireNonNull(value), version, UpdaterState.READ);
    }

    public static LoginFailuresUpdater delete(LoginFailureKey key) {
        return new LoginFailuresUpdater(key, null, NO_VERSION, UpdaterState.DELETED);
    }

    @Override
    public Expiration computeExpiration() {
        RealmModel realm = KeycloakSessionUtil.getKeycloakSession().getContext().getRealm();
        return new Expiration(
                SessionTimeouts.getLoginFailuresMaxIdleMs(realm, null, getValue()),
                SessionTimeouts.getLoginFailuresLifespanMs(realm, null, getValue()));
    }

    @Override
    public LoginFailureEntity apply(LoginFailureKey ignored, LoginFailureEntity cachedEntity) {
        assert !isDeleted();
        assert !isReadOnly();
        if (cachedEntity == null) {
            //entity removed
            return null;
        }
        changes.forEach(c -> c.accept(cachedEntity));
        return cachedEntity;
    }


    @Override
    public int getFailedLoginNotBefore() {
        return getValue().getFailedLoginNotBefore();
    }

    @Override
    public long getLastFailure() {
        return getValue().getLastFailure();
    }

    @Override
    public String getLastIPFailure() {
        return getValue().getLastIPFailure();
    }

    @Override
    public int getNumFailures() {
        return getValue().getNumFailures();
    }

    @Override
    public int getNumTemporaryLockouts() {
        return getValue().getNumTemporaryLockouts();
    }

    @Override
    public String getUserId() {
        return getValue().getUserId();
    }

    @Override
    public String getId() {
        return getKey().toString();
    }

    @Override
    public void clearFailures() {
        changes.clear();
        addAndApplyChange(CLEAR);
    }

    @Override
    public void setFailedLoginNotBefore(int notBefore) {
        addAndApplyChange(e -> e.setFailedLoginNotBefore(notBefore));
    }

    @Override
    public void incrementFailures() {
        addAndApplyChange(INCREMENT_FAILURES);
    }

    @Override
    public void incrementTemporaryLockouts() {
        addAndApplyChange(INCREMENT_LOCK_OUTS);
    }

    @Override
    public void setLastFailure(long lastFailure) {
        addAndApplyChange(e -> e.setLastFailure(lastFailure));
    }

    @Override
    public void setLastIPFailure(String ip) {
        addAndApplyChange(e -> e.setLastIPFailure(ip));
    }

    @Override
    public int getNumSecondaryAuthFailures() {
        return getValue().getNumSecondaryAuthFailures();
    }

    @Override
    public void incrementSecondaryAuthFailures() {
        addAndApplyChange(INCREMENT_SECONDARY_AUTH_FAILURES);
    }

    @Override
    public void clearPrimaryAndSecondaryAuthFailures() {
        changes.clear();
        addAndApplyChange(CLEAR_PRIMARY_AND_SECONDARY_AUTH_FAILURES);
    }

    @Override
    protected boolean isUnchanged() {
        return changes.isEmpty();
    }

    private void addAndApplyChange(Consumer<LoginFailureEntity> change) {
        changes.add(change);
        change.accept(getValue());
    }

    private static final Consumer<LoginFailureEntity> CLEAR = LoginFailureEntity::clearFailures;
    private static final Consumer<LoginFailureEntity> CLEAR_PRIMARY_AND_SECONDARY_AUTH_FAILURES = LoginFailureEntity::clearPrimaryAndSecondaryAuthFailures;
    private static final Consumer<LoginFailureEntity> INCREMENT_FAILURES = e -> e.setNumFailures(e.getNumFailures() + 1);
    private static final Consumer<LoginFailureEntity> INCREMENT_SECONDARY_AUTH_FAILURES = e -> e.setNumSecondaryAuthFailures(e.getNumSecondaryAuthFailures() + 1);
    private static final Consumer<LoginFailureEntity> INCREMENT_LOCK_OUTS = e -> e.setNumTemporaryLockouts(e.getNumTemporaryLockouts() + 1);
}
