// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.resourcemanager.authorization.implementation;

import com.azure.resourcemanager.authorization.AuthorizationManager;
import com.azure.resourcemanager.authorization.fluent.models.KeyCredentialInner;
import com.azure.resourcemanager.authorization.fluent.models.PasswordCredentialInner;
import com.azure.resourcemanager.authorization.fluent.models.ServicePrincipalInner;
import com.azure.resourcemanager.authorization.models.ActiveDirectoryApplication;
import com.azure.resourcemanager.authorization.models.BuiltInRole;
import com.azure.resourcemanager.authorization.models.CertificateCredential;
import com.azure.resourcemanager.authorization.models.KeyCredentialsUpdateParameters;
import com.azure.resourcemanager.authorization.models.PasswordCredential;
import com.azure.resourcemanager.authorization.models.PasswordCredentialsUpdateParameters;
import com.azure.resourcemanager.authorization.models.RoleAssignment;
import com.azure.resourcemanager.authorization.models.ServicePrincipal;
import com.azure.resourcemanager.authorization.models.ServicePrincipalCreateParameters;
import com.azure.resourcemanager.resources.fluentcore.model.Creatable;
import com.azure.resourcemanager.resources.fluentcore.model.implementation.CreatableUpdatableImpl;
import com.azure.resourcemanager.resources.models.ResourceGroup;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;

/** Implementation for ServicePrincipal and its parent interfaces. */
class ServicePrincipalImpl extends CreatableUpdatableImpl<ServicePrincipal, ServicePrincipalInner, ServicePrincipalImpl>
    implements ServicePrincipal,
        ServicePrincipal.Definition,
        ServicePrincipal.Update,
        HasCredential<ServicePrincipalImpl> {
    private AuthorizationManager manager;

    private Map<String, PasswordCredential> cachedPasswordCredentials;
    private Map<String, CertificateCredential> cachedCertificateCredentials;
    private Map<String, RoleAssignment> cachedRoleAssignments;

    private ServicePrincipalCreateParameters createParameters;
    private Creatable<ActiveDirectoryApplication> applicationCreatable;
    private Map<String, BuiltInRole> rolesToCreate;
    private Set<String> rolesToDelete;

    String assignedSubscription;
    private List<CertificateCredentialImpl<?>> certificateCredentialsToCreate;
    private List<PasswordCredentialImpl<?>> passwordCredentialsToCreate;
    private Set<String> certificateCredentialsToDelete;
    private Set<String> passwordCredentialsToDelete;

    ServicePrincipalImpl(ServicePrincipalInner innerObject, AuthorizationManager manager) {
        super(innerObject.displayName(), innerObject);
        this.manager = manager;
        this.createParameters = new ServicePrincipalCreateParameters();
        this.createParameters.withAccountEnabled(true);
        this.cachedRoleAssignments = new HashMap<>();
        this.rolesToCreate = new HashMap<>();
        this.rolesToDelete = new HashSet<>();
        this.cachedCertificateCredentials = new HashMap<>();
        this.certificateCredentialsToCreate = new ArrayList<>();
        this.certificateCredentialsToDelete = new HashSet<>();
        this.cachedPasswordCredentials = new HashMap<>();
        this.passwordCredentialsToCreate = new ArrayList<>();
        this.passwordCredentialsToDelete = new HashSet<>();
    }

    @Override
    public String applicationId() {
        return innerModel().appId();
    }

    @Override
    public List<String> servicePrincipalNames() {
        return innerModel().servicePrincipalNames();
    }

    @Override
    public Map<String, PasswordCredential> passwordCredentials() {
        return Collections.unmodifiableMap(cachedPasswordCredentials);
    }

    @Override
    public Map<String, CertificateCredential> certificateCredentials() {
        return Collections.unmodifiableMap(cachedCertificateCredentials);
    }

    @Override
    public Set<RoleAssignment> roleAssignments() {
        return Collections.unmodifiableSet(new HashSet<>(cachedRoleAssignments.values()));
    }

    @Override
    protected Mono<ServicePrincipalInner> getInnerAsync() {
        return manager.serviceClient().getServicePrincipals().getAsync(id(), this.manager.tenantId());
    }

    @Override
    public Mono<ServicePrincipal> createResourceAsync() {
        Mono<ServicePrincipal> sp = Mono.just(this);
        if (isInCreateMode()) {
            if (applicationCreatable != null) {
                ActiveDirectoryApplication application = this.taskResult(applicationCreatable.key());
                createParameters.withAppId(application.applicationId());
            }
            sp = manager.serviceClient().getServicePrincipals()
                .createAsync(this.manager.tenantId(), createParameters).map(innerToFluentMap(this));
        }
        return sp
            .flatMap(
                servicePrincipal ->
                    submitCredentialsAsync(servicePrincipal).mergeWith(submitRolesAsync(servicePrincipal)).last())
            .map(
                servicePrincipal -> {
                    for (PasswordCredentialImpl<?> passwordCredential : passwordCredentialsToCreate) {
                        passwordCredential.exportAuthFile((ServicePrincipalImpl) servicePrincipal);
                    }
                    for (CertificateCredentialImpl<?> certificateCredential : certificateCredentialsToCreate) {
                        certificateCredential.exportAuthFile((ServicePrincipalImpl) servicePrincipal);
                    }
                    passwordCredentialsToCreate.clear();
                    certificateCredentialsToCreate.clear();
                    return servicePrincipal;
                });
    }

    private Mono<ServicePrincipal> submitCredentialsAsync(final ServicePrincipal sp) {
        Mono<ServicePrincipal> mono = Mono.empty();
        if (!certificateCredentialsToCreate.isEmpty() || !certificateCredentialsToDelete.isEmpty()) {
            Map<String, CertificateCredential> newCerts = new HashMap<>(cachedCertificateCredentials);
            for (String delete : certificateCredentialsToDelete) {
                newCerts.remove(delete);
            }
            for (CertificateCredential create : certificateCredentialsToCreate) {
                newCerts.put(create.name(), create);
            }
            List<KeyCredentialInner> updateKeyCredentials = new ArrayList<>();
            for (CertificateCredential certificateCredential : newCerts.values()) {
                updateKeyCredentials.add(certificateCredential.innerModel());
            }
            mono =
                mono
                    .concatWith(
                        manager()
                            .serviceClient()
                            .getServicePrincipals()
                            .updateKeyCredentialsAsync(
                                sp.id(),
                                this.manager.tenantId(),
                                new KeyCredentialsUpdateParameters().withValue(updateKeyCredentials))
                            .then(Mono.just(ServicePrincipalImpl.this)))
                    .last();
        }
        if (!passwordCredentialsToCreate.isEmpty() || !passwordCredentialsToDelete.isEmpty()) {
            Map<String, PasswordCredential> newPasses = new HashMap<>(cachedPasswordCredentials);
            for (String delete : passwordCredentialsToDelete) {
                newPasses.remove(delete);
            }
            for (PasswordCredential create : passwordCredentialsToCreate) {
                newPasses.put(create.name(), create);
            }
            List<PasswordCredentialInner> updatePasswordCredentials = new ArrayList<>();
            for (PasswordCredential passwordCredential : newPasses.values()) {
                updatePasswordCredentials.add(passwordCredential.innerModel());
            }
            mono =
                mono
                    .concatWith(
                        manager()
                            .serviceClient()
                            .getServicePrincipals()
                            .updatePasswordCredentialsAsync(
                                sp.id(),
                                this.manager.tenantId(),
                                new PasswordCredentialsUpdateParameters().withValue(updatePasswordCredentials))
                            .then(Mono.just(ServicePrincipalImpl.this)))
                    .last();
        }
        return mono
            .flatMap(
                servicePrincipal -> {
                    passwordCredentialsToDelete.clear();
                    certificateCredentialsToDelete.clear();
                    return refreshCredentialsAsync();
                });
    }

    private Mono<ServicePrincipal> submitRolesAsync(final ServicePrincipal servicePrincipal) {
        Mono<ServicePrincipal> create;
        if (rolesToCreate.isEmpty()) {
            create = Mono.just(servicePrincipal);
        } else {
            create =
                Flux
                    .fromIterable(rolesToCreate.entrySet())
                    .flatMap(
                        roleEntry ->
                            manager()
                                .roleAssignments()
                                .define(this.manager().internalContext().randomUuid())
                                .forServicePrincipal(servicePrincipal)
                                .withBuiltInRole(roleEntry.getValue())
                                .withScope(roleEntry.getKey())
                                .createAsync())
                    .doOnNext(
                        indexable ->
                            cachedRoleAssignments.put(indexable.id(), indexable))
                    .last()
                    .map(
                        indexable -> {
                            rolesToCreate.clear();
                            return servicePrincipal;
                        });
        }
        Mono<ServicePrincipal> delete;
        if (rolesToDelete.isEmpty()) {
            delete = Mono.just(servicePrincipal);
        } else {
            delete =
                Flux
                    .fromIterable(rolesToDelete)
                    .flatMap(
                        role ->
                            manager()
                                .roleAssignments()
                                .deleteByIdAsync(cachedRoleAssignments.get(role).id())
                                .thenReturn(role))
                    .doOnNext(s -> cachedRoleAssignments.remove(s))
                    .last()
                    .map(
                        s -> {
                            rolesToDelete.clear();
                            return servicePrincipal;
                        });
        }
        return create.mergeWith(delete).last();
    }

    @Override
    public boolean isInCreateMode() {
        return id() == null;
    }

    Mono<ServicePrincipal> refreshCredentialsAsync() {
        return Mono
            .just(ServicePrincipalImpl.this)
            .map(
                (Function<ServicePrincipalImpl, ServicePrincipal>)
                servicePrincipal -> {
                    servicePrincipal.cachedCertificateCredentials.clear();
                    servicePrincipal.cachedPasswordCredentials.clear();
                    return servicePrincipal;
                })
            .concatWith(
                manager()
                    .serviceClient()
                    .getServicePrincipals()
                    .listKeyCredentialsAsync(id(), this.manager.tenantId())
                    .map(
                        keyCredentialInner -> {
                            CertificateCredential credential = new CertificateCredentialImpl<>(keyCredentialInner);
                            ServicePrincipalImpl.this.cachedCertificateCredentials.put(credential.name(), credential);
                            return ServicePrincipalImpl.this;
                        }))
            .concatWith(
                manager()
                    .serviceClient()
                    .getServicePrincipals()
                    .listPasswordCredentialsAsync(id(), this.manager.tenantId())
                    .map(
                        passwordCredentialInner -> {
                            PasswordCredential credential = new PasswordCredentialImpl<>(passwordCredentialInner);
                            ServicePrincipalImpl.this.cachedPasswordCredentials.put(credential.name(), credential);
                            return ServicePrincipalImpl.this;
                        }))
            .last();
    }

    @Override
    public Mono<ServicePrincipal> refreshAsync() {
        return getInnerAsync().map(innerToFluentMap(this)).flatMap(application -> refreshCredentialsAsync());
    }

    @Override
    public CertificateCredentialImpl<ServicePrincipalImpl> defineCertificateCredential(String name) {
        return new CertificateCredentialImpl<>(name, this);
    }

    @Override
    public PasswordCredentialImpl<ServicePrincipalImpl> definePasswordCredential(String name) {
        return new PasswordCredentialImpl<>(name, this);
    }

    @Override
    public ServicePrincipalImpl withoutCredential(String name) {
        if (cachedPasswordCredentials.containsKey(name)) {
            passwordCredentialsToDelete.add(name);
        } else if (cachedCertificateCredentials.containsKey(name)) {
            certificateCredentialsToDelete.add(name);
        }
        return this;
    }

    @Override
    public ServicePrincipalImpl withCertificateCredential(CertificateCredentialImpl<?> credential) {
        this.certificateCredentialsToCreate.add(credential);
        return this;
    }

    @Override
    public ServicePrincipalImpl withPasswordCredential(PasswordCredentialImpl<?> credential) {

        this.passwordCredentialsToCreate.add(credential);
        return this;
    }

    @Override
    public ServicePrincipalImpl withExistingApplication(String id) {
        createParameters.withAppId(id);
        return this;
    }

    @Override
    public ServicePrincipalImpl withExistingApplication(ActiveDirectoryApplication application) {
        createParameters.withAppId(application.applicationId());
        return this;
    }

    @Override
    public ServicePrincipalImpl withNewApplication(Creatable<ActiveDirectoryApplication> applicationCreatable) {
        this.addDependency(applicationCreatable);
        this.applicationCreatable = applicationCreatable;
        return this;
    }

    @Override
    public ServicePrincipalImpl withNewApplication(String signOnUrl) {
        return withNewApplication(
            manager.applications().define(name()).withSignOnUrl(signOnUrl).withIdentifierUrl(signOnUrl));
    }

    @Override
    public ServicePrincipalImpl withNewRole(BuiltInRole role, String scope) {
        this.rolesToCreate.put(scope, role);
        return this;
    }

    @Override
    public ServicePrincipalImpl withNewRoleInSubscription(BuiltInRole role, String subscriptionId) {
        this.assignedSubscription = subscriptionId;
        return withNewRole(role, "subscriptions/" + subscriptionId);
    }

    @Override
    public ServicePrincipalImpl withNewRoleInResourceGroup(BuiltInRole role, ResourceGroup resourceGroup) {
        return withNewRole(role, resourceGroup.id());
    }

    @Override
    public Update withoutRole(RoleAssignment roleAssignment) {
        this.rolesToDelete.add(roleAssignment.id());
        return this;
    }

    @Override
    public String id() {
        return innerModel().objectId();
    }

    @Override
    public AuthorizationManager manager() {
        return this.manager;
    }
}
