/*
 * Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * WSO2 Inc. 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.wso2.carbon.apimgt.impl.certificatemgt;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.apimgt.api.APIManagementException;
import org.wso2.carbon.apimgt.api.dto.CertificateInformationDTO;
import org.wso2.carbon.apimgt.api.dto.CertificateMetadataDTO;
import org.wso2.carbon.apimgt.api.dto.ClientCertificateDTO;
import org.wso2.carbon.apimgt.api.model.Identifier;
import org.wso2.carbon.apimgt.impl.certificatemgt.exceptions.CertificateAliasExistsException;
import org.wso2.carbon.apimgt.impl.certificatemgt.exceptions.CertificateManagementException;
import org.wso2.carbon.apimgt.impl.dao.CertificateMgtDAO;
import org.wso2.carbon.apimgt.impl.utils.CertificateMgtUtils;
import org.wso2.carbon.base.MultitenantConstants;
import org.wso2.carbon.context.CarbonContext;
import org.wso2.carbon.context.PrivilegedCarbonContext;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.util.ArrayList;
import java.util.List;

/**
 * This class holds the implementation of the CertificateManager interface.
 */
public class CertificateManagerImpl implements CertificateManager {

    private static Log log = LogFactory.getLog(CertificateManagerImpl.class);
    private static final String PROFILE_CONFIG = "sslprofiles.xml";
    private static final String CARBON_HOME_STRING = "carbon.home";
    private static String listenerProfileFilePath;
    private CertificateMgtDAO certificateMgtDAO = CertificateMgtDAO.getInstance();
    private CertificateMgtUtils certificateMgtUtils = CertificateMgtUtils.getInstance();
    private static CertificateManagerImpl instance;
    private String senderProfilePath;

    /**
     * To get the instance of certificate manager.
     *
     * @return instance of certificate manager.
     */
    public static CertificateManagerImpl getInstance() {

        if (instance == null) {
            synchronized (CertificateManagerImpl.class) {
                if (instance == null) {
                    instance = new CertificateManagerImpl();
                }
            }
        }
        return instance;
    }

    /**
     * Initializes an instance of CertificateManagerImpl.
     */
    private CertificateManagerImpl() {
        listenerProfileFilePath = CertificateMgtUtils.getSSLListenerProfilePath();
        senderProfilePath = CertificateMgtUtils.getSSLSenderProfilePath();
    }

    @Override
    public ResponseCode addCertificateToParentNode(String certificate, String alias, String endpoint, int tenantId) {

        try {
            if (certificateMgtDAO.addCertificate(certificate, alias, endpoint, tenantId)) {
                ResponseCode responseCode = certificateMgtUtils.addCertificateToTrustStore(certificate, alias);
                if (responseCode.getResponseCode() ==
                        ResponseCode.INTERNAL_SERVER_ERROR.getResponseCode()) {
                    log.error("Error adding the certificate to Publisher Trust Store. Rolling back...");
                    certificateMgtDAO.deleteCertificate(alias, endpoint, tenantId);
                } else if (responseCode.getResponseCode() == ResponseCode.ALIAS_EXISTS_IN_TRUST_STORE
                        .getResponseCode()) {
                    log.error("Could not add Certificate to Trust Store. Certificate Exists. Rolling back...");
                    certificateMgtDAO.deleteCertificate(alias, endpoint, tenantId);
                } else if (responseCode.getResponseCode() == ResponseCode.CERTIFICATE_EXPIRED.getResponseCode()) {
                    log.error("Could not add Certificate. Certificate has already expired.");
                    certificateMgtDAO.deleteCertificate(alias, endpoint, tenantId);
                } else {
                    log.info("Certificate is successfully added to the Publisher client Trust Store with Alias '"
                            + alias + "'");
                }
                return responseCode;
            } else {
                log.error("Error persisting the certificate meta data in db. Certificate could not be added to " +
                        "publisher Trust Store.");
                return ResponseCode.INTERNAL_SERVER_ERROR;
            }
        } catch (CertificateManagementException e) {
            log.error("Error when persisting/ deleting certificate metadata. ", e);
            return ResponseCode.INTERNAL_SERVER_ERROR;
        } catch (CertificateAliasExistsException e) {
            return ResponseCode.ALIAS_EXISTS_IN_TRUST_STORE;
        }
    }

    @Override
    public ResponseCode addClientCertificate(Identifier apiIdentifier, String certificate, String alias,
                                             String tierName, int tenantId, String organization) {

        ResponseCode responseCode;
        try {
            responseCode = certificateMgtUtils.validateCertificate(alias, tenantId, certificate);
            if (responseCode == ResponseCode.SUCCESS) {
                if (certificateMgtDAO.checkWhetherAliasExist(alias, tenantId)) {
                    responseCode = ResponseCode.ALIAS_EXISTS_IN_TRUST_STORE;
                } else {
                    certificateMgtDAO
                            .addClientCertificate(certificate, apiIdentifier, alias, tierName, tenantId, organization);
                }
            }
        } catch (CertificateManagementException e) {
            log.error("Error when adding client certificate with alias " + alias + " to database for the API "
                    + apiIdentifier.toString(), e);
            responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
        }
        return responseCode;
    }

    @Override
    public ResponseCode deleteCertificateFromParentNode(String alias, String endpoint, int tenantId) {

        try {
            List<CertificateMetadataDTO> certificateMetadataDTOList =
                    certificateMgtDAO.getCertificates(alias, null, tenantId);
            if (certificateMetadataDTOList != null && certificateMetadataDTOList.size() == 1) {
                CertificateMetadataDTO certificate = certificateMetadataDTOList.get(0);
                boolean removeFromDB = certificateMgtDAO.deleteCertificate(alias, endpoint, tenantId);
                if (removeFromDB) {
                    ResponseCode responseCode = certificateMgtUtils.removeCertificateFromTrustStore(alias);
                    if (responseCode == ResponseCode.INTERNAL_SERVER_ERROR) {
                        certificateMgtDAO.addCertificate(certificate.getCertificate(), alias, endpoint, tenantId);
                        log.error("Error removing the Certificate from Trust Store. Rolling back...");
                    } else if (responseCode.getResponseCode() == ResponseCode.CERTIFICATE_NOT_FOUND.getResponseCode()) {
                        log.warn("The Certificate for Alias '" + alias + "' has been previously removed from " +
                                "Trust Store. Hence DB entry is removed.");
                    } else {
                        log.info("Certificate is successfully removed from the Publisher Trust Store with Alias '"
                                + alias + "'");
                    }
                    return responseCode;
                } else {
                    log.error(
                            "Failed to remove certificate from the data base. No certificate changes will be affected" +
                                    ".");
                    return ResponseCode.INTERNAL_SERVER_ERROR;
                }
            }
        } catch (CertificateManagementException e) {
            log.error("Error persisting/ deleting certificate metadata. ", e);
            return ResponseCode.INTERNAL_SERVER_ERROR;
        } catch (CertificateAliasExistsException e) {
            return ResponseCode.ALIAS_EXISTS_IN_TRUST_STORE;
        }
        return ResponseCode.CERTIFICATE_NOT_FOUND;
    }

    @Override
    public ResponseCode deleteClientCertificateFromParentNode(Identifier apiIdentifier, String alias, int tenantId) {

        try {
            boolean removeFromDB = certificateMgtDAO.deleteClientCertificate(apiIdentifier, alias, tenantId);
            if (removeFromDB) {
                return ResponseCode.SUCCESS;
            } else {
                log.error("Failed to remove certificate with alias " + alias + " from the database for the API "
                        + apiIdentifier + "  No certificate changes will be affected.");
                return ResponseCode.INTERNAL_SERVER_ERROR;
            }
        } catch (CertificateManagementException e) {
            log.error(
                    "Error while deleting certificate metadata of the alias " + alias + " of the API " + apiIdentifier,
                    e);
            return ResponseCode.INTERNAL_SERVER_ERROR;
        }
    }

    @Override
    public boolean addCertificateToGateway(String certificate, String alias) {
        // Check whether the api is invoked via the APIGatewayAdmin service.
        int loggedInTenantId = CarbonContext.getThreadLocalCarbonContext().getTenantId();
        if (loggedInTenantId != MultitenantConstants.SUPER_TENANT_ID) {
            alias = alias + "_" + loggedInTenantId;
        }
        return addCertificateToListenerOrSenderProfile(certificate, alias, false);
    }

    @Override
    public boolean addClientCertificateToGateway(String certificate, String alias) {

        int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId();
        /*
        Tenant ID is appended with alias to make sure, only the admins from the same tenant, can delete the
        certificates later.
         */
        if (alias.endsWith("_" + tenantId) ||
                tenantId == org.wso2.carbon.utils.multitenancy.MultitenantConstants.SUPER_TENANT_ID) {
            return addCertificateToListenerOrSenderProfile(certificate, alias, true);
        } else {
            log.warn("Attempt to add an alias " + alias + " by tenant " + tenantId + " has been rejected. Please "
                    + "make sure to provide a alias name that ends with '_" + tenantId + "' .");
            return false;
        }
    }

    /**
     * To add the public certificate to the relevant listener of sender profile.
     *
     * @param certificate Relevant certificate that need to be added to the trust store of gateway.
     * @param alias       Alias of the certificate.
     * @param isListener  To indicate whether the listener profile need to be reloaded.
     * @return true if the addition to gateway certificate addition succeeded.
     */
    private boolean addCertificateToListenerOrSenderProfile(String certificate, String alias, boolean isListener) {

        boolean result;
        boolean fileUpdateSucceed;

        if (isListener) {
            ResponseCode responseCode = certificateMgtUtils.addCertificateToListenerTrustStore(certificate, alias);
            if (responseCode == ResponseCode.ALIAS_EXISTS_IN_TRUST_STORE) {
                log.info("The Alias '" + alias + "' exists in the Gateway Trust Store.");
                result = true;
            } else {
                result = responseCode != ResponseCode.INTERNAL_SERVER_ERROR;
            }
            fileUpdateSucceed = touchSSLListenerConfigFile();
        } else {
            ResponseCode responseCode = certificateMgtUtils.addCertificateToSenderTrustStore(certificate, alias);
            if (responseCode == ResponseCode.ALIAS_EXISTS_IN_TRUST_STORE) {
                log.info("The Alias '" + alias + "' exists in the Gateway Trust Store.");
                result = true;
            } else {
                result = responseCode != ResponseCode.INTERNAL_SERVER_ERROR;
            }
            fileUpdateSucceed = touchSSLSenderConfigFile();
        }
        result = result && fileUpdateSucceed;
        if (result) {
            log.info("The certificate with Alias '" + alias + "' is successfully added to the Gateway "
                    + "Trust Store.");
        } else {
            log.error("Error adding the certificate with Alias '" + alias + "' to the Gateway Trust Store");
        }
        return result;
    }

    @Override
    public boolean deleteCertificateFromGateway(String alias) {
        // Check whether the api is invoked via the APIGatewayAdmin service.
        int loggedInTenantId = CarbonContext.getThreadLocalCarbonContext().getTenantId();
        if (loggedInTenantId != MultitenantConstants.SUPER_TENANT_ID) {
            alias = alias + "_" + loggedInTenantId;
        }
        return deleteCertificateFromListenerAndSenderProfiles(alias, false);
    }

    @Override
    public boolean deleteClientCertificateFromGateway(String alias) {

        int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId();
        /*
            Tenant ID is checked to make sure that tenant admins cannot delete the alias that do not belong their
            tenant. Super tenant is special cased, as it is required to delete the certificates from different tenants.
         */
        if (alias.endsWith("_" + tenantId) ||
                tenantId == org.wso2.carbon.utils.multitenancy.MultitenantConstants.SUPER_TENANT_ID) {
            return deleteCertificateFromListenerAndSenderProfiles(alias, true);
        } else {
            log.warn("Attempt to delete the alias " + alias + " by tenant " + tenantId + " has been rejected. Only "
                    + "the client certificates that belongs to " + tenantId + " can be deleted. All the client "
                    + "certificates belongs to " + tenantId + " have '_" + tenantId + "' suffix in alias");
            return false;
        }
    }

    /**
     * To delete the certificate from http listener or sender profile.
     *
     * @param alias      Alias that need to be removed.
     * @param isListener To indicate whether http listener need to be updated or sender.
     * @return true if the the update of profile succeeded.
     */
    private boolean deleteCertificateFromListenerAndSenderProfiles(String alias, boolean isListener) {

        if (isListener) {
            ResponseCode responseCode = certificateMgtUtils.removeCertificateFromListenerTrustStore(alias);
            if (responseCode != ResponseCode.INTERNAL_SERVER_ERROR) {
                log.info("The certificate with Alias '" + alias + "' is successfully removed from the Gateway "
                        + "Trust Store.");
            } else {
                log.error(
                        "Error removing the certificate with Alias '" + alias + "' from the Gateway " + "Trust Store.");
                return false;
            }
            return touchSSLListenerConfigFile();
        } else {
            ResponseCode responseCode = certificateMgtUtils.removeCertificateFromSenderTrustStore(alias);
            if (responseCode != ResponseCode.INTERNAL_SERVER_ERROR) {
                log.info("The certificate with Alias '" + alias + "' is successfully removed from the Gateway "
                        + "Trust Store.");
            } else {
                log.error(
                        "Error removing the certificate with Alias '" + alias + "' from the Gateway " + "Trust Store.");
                return false;
            }

            return touchSSLSenderConfigFile();
        }
    }

    @Override
    public boolean isConfigured() {

        return true;
    }

    @Override
    public CertificateMetadataDTO getCertificate(String alias, int tenantId) {

        List<CertificateMetadataDTO> certificateMetadataList = null;
        CertificateMetadataDTO certificate = null;

        try {
            if (log.isDebugEnabled()) {
                log.debug("Retrieving certificate metadata for alias: " + alias + " and tenant: " + tenantId);
            }
            certificateMetadataList = certificateMgtDAO.getCertificates(alias, "", tenantId);
            if (certificateMetadataList != null && certificateMetadataList.size() == 1) {
                certificate = certificateMetadataList.get(0);
            }
        } catch (CertificateManagementException e) {
            log.error("Error when retrieving certificate metadata for alias '" + alias + "'", e);
        }
        return certificate;
    }

    @Override
    public List<CertificateMetadataDTO> getCertificates(String endpoint, int tenantId) {

        List<CertificateMetadataDTO> certificateMetadataList = null;
        try {
            certificateMetadataList = certificateMgtDAO.getCertificates("", endpoint, tenantId);
        } catch (CertificateManagementException e) {
            log.error("Error when retrieving certificate metadata for endpoint '" + endpoint + "'", e);
        }
        return certificateMetadataList;
    }

    @Override
    public List<CertificateMetadataDTO> getCertificates(int tenantId) {

        List<CertificateMetadataDTO> certificates = null;

        if (log.isDebugEnabled()) {
            log.debug("Get all the certificates for tenant " + tenantId);
        }
        try {
            certificates = certificateMgtDAO.getCertificates(null, null, tenantId);
        } catch (CertificateManagementException e) {
            log.error("Error retrieving certificates for the tenantId '" + tenantId + "' ", e);
        }
        return certificates;
    }

    @Override
    public List<CertificateMetadataDTO> getCertificates(int tenantId, String alias, String endpoint)
            throws APIManagementException {

        List<CertificateMetadataDTO> certificateMetadataList = new ArrayList<>();

        if (log.isDebugEnabled()) {
            log.debug(String.format("Retrieve certificates of tenant %d which matches alias : %s and endpoint : %s",
                    tenantId, alias, endpoint));
        }
        try {
            certificateMetadataList = certificateMgtDAO.getCertificates(alias, endpoint, tenantId);
        } catch (CertificateManagementException e) {
            throw new APIManagementException("Error retrieving certificate information for tenantId '" + tenantId +
                    "' and alias '" + alias + "'");
        }
        return certificateMetadataList;
    }

    @Override
    public List<ClientCertificateDTO> searchClientCertificates(int tenantId, String alias,
                                                               Identifier apiIdentifier, String organization)
            throws APIManagementException {

        try {
            return CertificateMgtDAO.getInstance().getClientCertificates(tenantId, alias, apiIdentifier, organization);
        } catch (CertificateManagementException e) {
            throw new APIManagementException(
                    "Error while retrieving client certificate information for the tenant : " + tenantId, e);
        }
    }

    @Override
    public boolean isCertificatePresent(int tenantId, String alias) throws APIManagementException {

        List<CertificateMetadataDTO> certificateMetadataList;
        if (log.isDebugEnabled()) {
            log.debug(String.format("Check whether the tenant %d has a certificate for alias %s", tenantId, alias));
        }
        try {
            certificateMetadataList = certificateMgtDAO.getCertificates(alias, null, tenantId);
        } catch (CertificateManagementException e) {
            throw new APIManagementException("Error retrieving certificate information for tenantId '" + tenantId +
                    "' and alias '" + alias + "'");
        }
        return certificateMetadataList.size() == 1; // The list would not be null so we check the size.
    }

    @Override
    public CertificateInformationDTO getCertificateInformation(int tenantId, String alias) throws APIManagementException {

        if (log.isDebugEnabled()) {
            log.debug(String.format("Get Certificate information for alias %s", alias));
        }
        try {
            CertificateMetadataDTO metadataDTO = certificateMgtDAO.getCertificate(alias, tenantId);
            return certificateMgtUtils.getCertificateInfo(metadataDTO.getCertificate());
        } catch (CertificateManagementException e) {
            throw new APIManagementException(e);
        }
    }

    @Override
    public ResponseCode updateCertificate(String certificate, String alias) throws APIManagementException {

        try {
            return certificateMgtUtils.updateCertificate(certificate, alias);
        } catch (CertificateManagementException e) {
            throw new APIManagementException(e);
        }
    }

    @Override
    public ResponseCode updateClientCertificate(String certificate, String alias, String tier, int tenantId,
            String organization) throws APIManagementException {

        ResponseCode responseCode = ResponseCode.SUCCESS;
        if (StringUtils.isNotEmpty(certificate)) {
            responseCode = certificateMgtUtils.validateCertificate(null, tenantId, certificate);
        }
        try {
            if (responseCode.getResponseCode() == ResponseCode.SUCCESS.getResponseCode()) {
                boolean isSuccess = certificateMgtDAO
                        .updateClientCertificate(certificate, alias, tier, tenantId, organization);
                if (isSuccess) {
                    responseCode = ResponseCode.SUCCESS;
                } else {
                    responseCode = ResponseCode.INTERNAL_SERVER_ERROR;
                }
            }
        } catch (CertificateManagementException e) {
            throw new APIManagementException(
                    "Certificate management exception while trying to update the certificate of alias " + alias
                            + " of tenant " + tenantId, e);
        }
        return responseCode;
    }

    @Override
    public int getCertificateCount(int tenantId) throws APIManagementException {

        if (log.isDebugEnabled()) {
            log.debug(String.format("Get the number of certificates tenant %d has.", tenantId));
        }
        try {
            return certificateMgtDAO.getCertificateCount(tenantId);
        } catch (CertificateManagementException e) {
            throw new APIManagementException(e);
        }
    }

    @Override
    public int getClientCertificateCount(int tenantId) throws APIManagementException {

        try {
            return certificateMgtDAO.getClientCertificateCount(tenantId);
        } catch (CertificateManagementException e) {
            throw new APIManagementException(
                    "Certificate management exception while getting count of client certificates of the tenant "
                            + tenantId, e);
        }
    }

    @Override
    public ByteArrayInputStream getCertificateContent(int tenantId, String alias) throws APIManagementException {

        if (log.isDebugEnabled()) {
            log.debug(String.format("Get the contents of the certificate for alias %s", alias));
        }
        try {
            CertificateMetadataDTO metadataDTO = certificateMgtDAO.getCertificate(alias, tenantId);
            return certificateMgtUtils.getCertificateContentFromDB(metadataDTO.getCertificate());
        } catch (CertificateManagementException e) {
            throw new APIManagementException(e);
        }
    }

    /**
     * Modify the sslProfiles.xml file after modifying the certificate.
     *
     * @return : True if the file modification is success.
     */
    private boolean touchSSLSenderConfigFile() {

        boolean success = false;
        File file = new File(senderProfilePath);
        if (file.exists()) {
            success = file.setLastModified(System.currentTimeMillis());
            if (success) {
                log.info("The Transport Sender will be re-initialized in few minutes.");
            } else {
                if (log.isDebugEnabled()) {
                    log.debug("Error when modifying the sslprofiles.xml file");
                }
                log.error("Could not modify the file '" + PROFILE_CONFIG + "'");
            }
        } else {
            if (log.isDebugEnabled()) {
                log.debug("sslprofiles.xml file not found.");
            }
            log.error("Could not find the file '" + PROFILE_CONFIG + "'");
        }
        return success;
    }

    /**
     * Modify the listenerProfiles.xml file after modifying the certificate.
     *
     * @return : True if the file modification is success.
     */
    private boolean touchSSLListenerConfigFile() {

        boolean success = false;
        if (listenerProfileFilePath != null) {
            File file = new File(listenerProfileFilePath);
            if (file.exists()) {
                success = file.setLastModified(System.currentTimeMillis());
                if (success) {
                    log.info("The Transport listener will be re-initialized in few minutes.");
                } else {
                    if (log.isDebugEnabled()) {
                        log.debug(
                                "Error when modifying listener profile config file in path " + listenerProfileFilePath);
                    }
                    log.error("Could not modify the file listener profile config file");
                }
            }
        } else {
            log.warn("Mutual SSL file path for listener is not configured correctly in axis2.xml. Please recheck the "
                    + "relevant configuration under transport listener.");
        }
        return success;
    }

    @Override
    public boolean addAllCertificateToGateway(String certificate, String alias, int tenantId) {
        // Check whether the api is invoked via the APIGatewayAdmin service.
        alias = alias + "_" + tenantId;

        return addCertificateToListenerOrSenderProfile(certificate, alias, false);
    }

    @Override
    public List<CertificateMetadataDTO> getAllCertificates() {
        List<CertificateMetadataDTO> certificates = null;

        if (log.isDebugEnabled()) {
            log.debug("Get all the certificates");
        }
        try {
            certificates = certificateMgtDAO.getAllCertificates();
        } catch (CertificateManagementException e) {
            log.error("Error retrieving certificates ", e);
        }
        return certificates;
    }
}
