/**
 * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
 *
 * 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 io.gravitee.am.management.service.impl.upgrades;

import io.gravitee.am.model.Domain;
import io.gravitee.am.model.oauth2.Scope;
import io.gravitee.am.service.DomainService;
import io.gravitee.am.service.ScopeService;
import io.gravitee.am.service.model.NewSystemScope;
import io.gravitee.am.service.model.UpdateSystemScope;
import io.reactivex.Observable;
import io.reactivex.Single;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

import java.util.Optional;

/**
 * @author David BRASSELY (david.brassely at graviteesource.com)
 * @author GraviteeSource Team
 */
@Component
public class OpenIDScopeUpgrader implements Upgrader, Ordered {

    /**
     * Logger.
     */
    private final Logger logger = LoggerFactory.getLogger(OpenIDScopeUpgrader.class);

    @Autowired
    private DomainService domainService;

    @Autowired
    private ScopeService scopeService;

    @Override
    public boolean upgrade() {
        logger.info("Applying OIDC scope upgrade");
        domainService.findAll()
                .flatMapObservable(Observable::fromIterable)
                .flatMapSingle(this::createOrUpdateSystemScopes)
                .subscribe();
        return true;
    }

    private Single<Domain> createOrUpdateSystemScopes(Domain domain) {
        return Observable.fromArray(io.gravitee.am.common.oidc.Scope.values())
                .flatMapSingle(scope -> createSystemScope(domain.getId(), scope))
                .lastOrError()
                .map(scope -> domain);
    }

    private Single<Scope> createSystemScope(String domain, io.gravitee.am.common.oidc.Scope systemScope) {
        return scopeService.findByDomainAndKey(domain, systemScope.getKey())
                .map(scope -> Optional.of(scope))
                .defaultIfEmpty(Optional.empty())
                .flatMapSingle(optScope -> {
                    if (!optScope.isPresent()) {
                        logger.info("Create a new system scope key[{}] for domain[{}]", systemScope.getKey(), domain);
                        NewSystemScope scope = new NewSystemScope();
                        scope.setKey(systemScope.getKey());
                        scope.setClaims(systemScope.getClaims());
                        scope.setName(systemScope.getLabel());
                        scope.setDescription(systemScope.getDescription());
                        scope.setDiscovery(systemScope.isDiscovery());
                        return scopeService.create(domain, scope);
                    } else if (shouldUpdateSystemScope(optScope, systemScope)){
                        logger.info("Update a system scope key[{}] for domain[{}]", systemScope.getKey(), domain);
                        final Scope existingScope = optScope.get();
                        UpdateSystemScope scope = new UpdateSystemScope();
                        scope.setName(existingScope.getName() != null ? existingScope.getName() : systemScope.getLabel());
                        scope.setDescription(existingScope.getDescription() != null ? existingScope.getDescription() : systemScope.getDescription());
                        scope.setClaims(systemScope.getClaims());
                        scope.setExpiresIn(existingScope.getExpiresIn());
                        scope.setDiscovery(systemScope.isDiscovery());
                        return scopeService.update(domain, optScope.get().getId(), scope);
                    }
                    return Single.just(optScope.get());
                });
    }

    /**
     * Update System scope if it is not currently set as system or if discovery property does not match.
     * @param optScope
     * @param systemScope
     * @return
     */
    private boolean shouldUpdateSystemScope(Optional<Scope> optScope, io.gravitee.am.common.oidc.Scope systemScope) {
        //If not currently set as system or if discovery property does not match
        return !optScope.get().isSystem() || optScope.get().isDiscovery() != systemScope.isDiscovery();
    }

    @Override
    public int getOrder() {
        return 5;
    }
}