// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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.apache.cloudstack.cloudian;

import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;
import javax.naming.ConfigurationException;

import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.cloudian.api.CloudianIsEnabledCmd;
import org.apache.cloudstack.cloudian.api.CloudianSsoLoginCmd;
import org.apache.cloudstack.cloudian.client.CloudianClient;
import org.apache.cloudstack.cloudian.client.CloudianGroup;
import org.apache.cloudstack.cloudian.client.CloudianUser;
import org.apache.cloudstack.cloudian.client.CloudianUtils;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.framework.messagebus.MessageBus;
import org.apache.cloudstack.framework.messagebus.MessageSubscriber;
import org.apache.log4j.Logger;

import com.cloud.domain.Domain;
import com.cloud.domain.DomainVO;
import com.cloud.domain.dao.DomainDao;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.DomainManager;
import com.cloud.user.User;
import com.cloud.user.dao.AccountDao;
import com.cloud.user.dao.UserDao;
import com.cloud.utils.component.ComponentLifecycleBase;
import com.cloud.utils.exception.CloudRuntimeException;

public class CloudianConnectorImpl extends ComponentLifecycleBase implements CloudianConnector, Configurable {
    private static final Logger LOG = Logger.getLogger(CloudianConnectorImpl.class);

    @Inject
    private UserDao userDao;

    @Inject
    private AccountDao accountDao;

    @Inject
    private DomainDao domainDao;

    @Inject
    private MessageBus messageBus;

    /////////////////////////////////////////////////////
    //////////////// Plugin Methods /////////////////////
    /////////////////////////////////////////////////////

    private CloudianClient getClient() {
        try {
            return new CloudianClient(CloudianAdminHost.value(), CloudianAdminPort.value(), CloudianAdminProtocol.value(),
                    CloudianAdminUser.value(), CloudianAdminPassword.value(),
                    CloudianValidateSSLSecurity.value(), CloudianAdminApiRequestTimeout.value());
        } catch (final KeyStoreException | NoSuchAlgorithmException | KeyManagementException e) {
            LOG.error("Failed to create Cloudian API client due to: ", e);
        }
        throw new CloudRuntimeException("Failed to create and return Cloudian API client instance");
    }

    private boolean addGroup(final Domain domain) {
        if (domain == null || !isEnabled()) {
            return false;
        }
        final CloudianClient client = getClient();
        final CloudianGroup group = new CloudianGroup();
        group.setGroupId(domain.getUuid());
        group.setGroupName(domain.getPath());
        group.setActive(true);
        return client.addGroup(group);
    }

    private boolean removeGroup(final Domain domain) {
        if (domain == null || !isEnabled()) {
            return false;
        }
        final CloudianClient client = getClient();
        for (final CloudianUser user: client.listUsers(domain.getUuid())) {
            if (client.removeUser(user.getUserId(), domain.getUuid())) {
                LOG.error(String.format("Failed to remove Cloudian user id=%s, while removing Cloudian group id=%s", user.getUserId(), domain.getUuid()));
            }
        }
        for (int retry = 0; retry < 3; retry++) {
            if (client.removeGroup(domain.getUuid())) {
                return true;
            } else {
                LOG.warn("Failed to remove Cloudian group id=" + domain.getUuid() + ", retrying count=" + retry+1);
            }
        }
        LOG.warn("Failed to remove Cloudian group id=" + domain.getUuid() + ", please remove manually");
        return false;
    }

    private boolean addUserAccount(final Account account, final Domain domain) {
        if (account == null || domain == null || !isEnabled()) {
            return false;
        }
        final User accountUser = userDao.listByAccount(account.getId()).get(0);
        final CloudianClient client = getClient();
        final String fullName = String.format("%s %s (%s)", accountUser.getFirstname(), accountUser.getLastname(), account.getAccountName());
        final CloudianUser user = new CloudianUser();
        user.setUserId(account.getUuid());
        user.setGroupId(domain.getUuid());
        user.setFullName(fullName);
        user.setEmailAddr(accountUser.getEmail());
        user.setUserType(CloudianUser.USER);
        user.setActive(true);
        return client.addUser(user);
    }

    private boolean updateUserAccount(final Account account, final Domain domain, final CloudianUser existingUser) {
        if (account == null || domain == null || !isEnabled()) {
            return false;
        }
        final CloudianClient client = getClient();
        if (existingUser != null) {
            final User accountUser = userDao.listByAccount(account.getId()).get(0);
            final String fullName = String.format("%s %s (%s)", accountUser.getFirstname(), accountUser.getLastname(), account.getAccountName());
            if (!existingUser.getActive() || !existingUser.getFullName().equals(fullName) || !existingUser.getEmailAddr().equals(accountUser.getEmail())) {
                existingUser.setActive(true);
                existingUser.setFullName(fullName);
                existingUser.setEmailAddr(accountUser.getEmail());
                return client.updateUser(existingUser);
            }
            return true;
        }
        return false;
    }

    private boolean removeUserAccount(final Account account) {
        if (account == null || !isEnabled()) {
            return false;
        }
        final CloudianClient client = getClient();
        final Domain domain = domainDao.findById(account.getDomainId());
        for (int retry = 0; retry < 3; retry++) {
            if (client.removeUser(account.getUuid(), domain.getUuid())) {
                return true;
            } else {
                LOG.warn("Failed to remove Cloudian user id=" + account.getUuid() + " in group id=" + domain.getUuid() + ", retrying count=" + retry+1);
            }
        }
        LOG.warn("Failed to remove Cloudian user id=" + account.getUuid() + " in group id=" + domain.getUuid() + ", please remove manually");
        return false;
    }

    //////////////////////////////////////////////////
    //////////////// Plugin APIs /////////////////////
    //////////////////////////////////////////////////

    @Override
    public String getCmcUrl() {
        return String.format("%s://%s:%s/Cloudian/", CloudianCmcProtocol.value(),
                CloudianCmcHost.value(), CloudianCmcPort.value());
    }

    @Override
    public boolean isEnabled() {
        return CloudianConnectorEnabled.value();
    }

    @Override
    public String generateSsoUrl() {
        final Account caller = CallContext.current().getCallingAccount();
        final Domain domain = domainDao.findById(caller.getDomainId());

        String user = caller.getUuid();
        String group = domain.getUuid();

        if (caller.getAccountName().equals("admin") && caller.getRoleId() == RoleType.Admin.getId()) {
            user = CloudianCmcAdminUser.value();
            group = "0";
        }

        LOG.debug(String.format("Attempting Cloudian SSO with user id=%s, group id=%s", user, group));

        final CloudianUser ssoUser = getClient().listUser(user, group);
        if (ssoUser == null || !ssoUser.getActive()) {
            LOG.debug(String.format("Failed to find existing Cloudian user id=%s in group id=%s", user, group));
            final CloudianGroup ssoGroup = getClient().listGroup(group);
            if (ssoGroup == null) {
                LOG.debug(String.format("Failed to find existing Cloudian group id=%s, trying to add it", group));
                if (!addGroup(domain)) {
                    LOG.error("Failed to add missing Cloudian group id=" + group);
                    throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Aborting Cloudian SSO, failed to add group to Cloudian.");
                }
            }
            if (!addUserAccount(caller, domain)) {
                LOG.error("Failed to add missing Cloudian group id=" + group);
                throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Aborting Cloudian SSO, failed to add user to Cloudian.");
            }
            final CloudianUser addedSsoUser = getClient().listUser(user, group);
            if (addedSsoUser == null || !addedSsoUser.getActive()) {
                throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Aborting Cloudian SSO, failed to find mapped Cloudian user, please fix integration issues.");
            }
        } else if (!group.equals("0")) {
            updateUserAccount(caller, domain, ssoUser);
        }

        LOG.debug(String.format("Validated Cloudian SSO for Cloudian user id=%s, group id=%s", user, group));
        return CloudianUtils.generateSSOUrl(getCmcUrl(), user, group, CloudianSsoKey.value());
    }

    ///////////////////////////////////////////////////////////
    //////////////// Plugin Configuration /////////////////////
    ///////////////////////////////////////////////////////////

    @Override
    public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
        super.configure(name, params);

        if (!isEnabled()) {
            LOG.debug("Cloudian connector is disabled, skipping configuration");
            return true;
        }

        LOG.debug(String.format("Cloudian connector is enabled, completed configuration, integration is ready. " +
                        "Cloudian admin host:%s, port:%s, user:%s",
                CloudianAdminHost.value(), CloudianAdminPort.value(), CloudianAdminUser.value()));

        messageBus.subscribe(AccountManager.MESSAGE_ADD_ACCOUNT_EVENT, new MessageSubscriber() {
            @Override
            public void onPublishMessage(String senderAddress, String subject, Object args) {
                try {
                    final Map<Long, Long> accountGroupMap = (Map<Long, Long>) args;
                    final Long accountId = accountGroupMap.keySet().iterator().next();
                    final Account account = accountDao.findById(accountId);
                    final Domain domain = domainDao.findById(account.getDomainId());

                    if (!addUserAccount(account, domain)) {
                        LOG.warn(String.format("Failed to add account in Cloudian while adding CloudStack account=%s in domain=%s", account.getAccountName(), domain.getPath()));
                    }
                } catch (final Exception e) {
                    LOG.error("Caught exception while adding account in Cloudian: ", e);
                }
            }
        });

        messageBus.subscribe(AccountManager.MESSAGE_REMOVE_ACCOUNT_EVENT, new MessageSubscriber() {
            @Override
            public void onPublishMessage(String senderAddress, String subject, Object args) {
                try {
                    final Account account = accountDao.findByIdIncludingRemoved((Long) args);
                    if(!removeUserAccount(account))    {
                        LOG.warn(String.format("Failed to remove account to Cloudian while removing CloudStack account=%s, id=%s", account.getAccountName(), account.getId()));
                    }
                } catch (final Exception e) {
                    LOG.error("Caught exception while removing account in Cloudian: ", e);
                }
            }
        });

        messageBus.subscribe(DomainManager.MESSAGE_ADD_DOMAIN_EVENT, new MessageSubscriber() {
            @Override
            public void onPublishMessage(String senderAddress, String subject, Object args) {
                try {
                    final Domain domain = domainDao.findById((Long) args);
                    if (!addGroup(domain)) {
                        LOG.warn(String.format("Failed to add group in Cloudian while adding CloudStack domain=%s id=%s", domain.getPath(), domain.getId()));
                    }
                } catch (final Exception e) {
                    LOG.error("Caught exception adding domain/group in Cloudian: ", e);
                }
            }
        });

        messageBus.subscribe(DomainManager.MESSAGE_REMOVE_DOMAIN_EVENT, new MessageSubscriber() {
            @Override
            public void onPublishMessage(String senderAddress, String subject, Object args) {
                try {
                    final DomainVO domain = (DomainVO) args;
                    if (!removeGroup(domain)) {
                        LOG.warn(String.format("Failed to remove group in Cloudian while removing CloudStack domain=%s id=%s", domain.getPath(), domain.getId()));
                    }
                } catch (final Exception e) {
                    LOG.error("Caught exception while removing domain/group in Cloudian: ", e);
                }
            }
        });

        return true;
    }

    @Override
    public List<Class<?>> getCommands() {
        final List<Class<?>> cmdList = new ArrayList<Class<?>>();
        cmdList.add(CloudianIsEnabledCmd.class);
        if (!isEnabled()) {
            return cmdList;
        }
        cmdList.add(CloudianSsoLoginCmd.class);
        return cmdList;
    }

    @Override
    public String getConfigComponentName() {
        return CloudianConnector.class.getSimpleName();
    }

    @Override
    public ConfigKey<?>[] getConfigKeys() {
        return new ConfigKey<?>[] {
                CloudianConnectorEnabled,
                CloudianAdminHost,
                CloudianAdminPort,
                CloudianAdminUser,
                CloudianAdminPassword,
                CloudianAdminProtocol,
                CloudianAdminApiRequestTimeout,
                CloudianValidateSSLSecurity,
                CloudianCmcAdminUser,
                CloudianCmcHost,
                CloudianCmcPort,
                CloudianCmcProtocol,
                CloudianSsoKey
        };
    }
}
