/*
 *  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.tomcat.util.net;

import java.io.File;
import java.io.IOException;
import java.io.Serial;
import java.io.Serializable;
import java.security.KeyStore;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import javax.management.ObjectName;
import javax.net.ssl.X509KeyManager;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.net.openssl.ciphers.Authentication;
import org.apache.tomcat.util.res.StringManager;

public class SSLHostConfigCertificate implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    private static final Log log = LogFactory.getLog(SSLHostConfigCertificate.class);
    private static final StringManager sm = StringManager.getManager(SSLHostConfigCertificate.class);

    public static final Type DEFAULT_TYPE = Type.UNDEFINED;

    static final String DEFAULT_KEYSTORE_PROVIDER = System.getProperty("javax.net.ssl.keyStoreProvider");
    static final String DEFAULT_KEYSTORE_TYPE = System.getProperty("javax.net.ssl.keyStoreType", "JKS");
    private static final String DEFAULT_KEYSTORE_FILE =
            System.getProperty("user.home") + File.separator + ".keystore";
    private static final String DEFAULT_KEYSTORE_PASSWORD = "changeit";

    // Internal
    private ObjectName oname;

    /*
     *  OpenSSL can handle multiple certs in a single config so the reference to the context is at the virtual host
     *  level. JSSE can't so the reference is held here on the certificate. Typically, the SSLContext is generated from
     *  the configuration but, particularly in embedded scenarios, it can be provided directly.
     */
    private transient volatile SSLContext sslContextProvided;
    private transient volatile SSLContext sslContextGenerated;


    // Common
    private final SSLHostConfig sslHostConfig;
    private final Type type;
    private String certificateKeyPassword = null;
    private String certificateKeyPasswordFile = null;

    // JSSE
    private String certificateKeyAlias;
    private String certificateKeystorePassword = DEFAULT_KEYSTORE_PASSWORD;
    private String certificateKeystorePasswordFile = null;
    private String certificateKeystoreFile = DEFAULT_KEYSTORE_FILE;
    private String certificateKeystoreProvider = DEFAULT_KEYSTORE_PROVIDER;
    private String certificateKeystoreType = DEFAULT_KEYSTORE_TYPE;
    private transient KeyStore certificateKeystore = null;
    private transient X509KeyManager certificateKeyManager = null;

    // OpenSSL
    private String certificateChainFile;
    private String certificateFile;
    private String certificateKeyFile;

    // Certificate store type
    private StoreType storeType = null;

    public SSLHostConfigCertificate() {
        this(null, Type.UNDEFINED);
    }

    public SSLHostConfigCertificate(SSLHostConfig sslHostConfig, Type type) {
        this.sslHostConfig = sslHostConfig;
        this.type = type;
    }


    public SSLContext getSslContext() {
        if (sslContextProvided != null) {
            return sslContextProvided;
        }
        return sslContextGenerated;
    }


    public void setSslContext(SSLContext sslContext) {
        this.sslContextProvided = sslContext;
    }


    public SSLContext getSslContextGenerated() {
        return sslContextGenerated;
    }


    void setSslContextGenerated(SSLContext sslContext) {
        this.sslContextGenerated = sslContext;
    }


    public SSLHostConfig getSSLHostConfig() {
        return sslHostConfig;
    }


    // Internal

    public ObjectName getObjectName() {
        return oname;
    }


    public void setObjectName(ObjectName oname) {
        this.oname = oname;
    }


    // Common

    public Type getType() {
        return type;
    }


    public String getCertificateKeyPassword() {
        return certificateKeyPassword;
    }


    public void setCertificateKeyPassword(String certificateKeyPassword) {
        this.certificateKeyPassword = certificateKeyPassword;
    }


    public String getCertificateKeyPasswordFile() {
        return certificateKeyPasswordFile;
    }


    public void setCertificateKeyPasswordFile(String certificateKeyPasswordFile) {
        this.certificateKeyPasswordFile = certificateKeyPasswordFile;
    }


    // JSSE

    public void setCertificateKeyAlias(String certificateKeyAlias) {
        sslHostConfig.setProperty(
                "Certificate.certificateKeyAlias", SSLHostConfig.Type.JSSE);
        this.certificateKeyAlias = certificateKeyAlias;
    }


    public String getCertificateKeyAlias() {
        return certificateKeyAlias;
    }


    public void setCertificateKeystoreFile(String certificateKeystoreFile) {
        sslHostConfig.setProperty(
                "Certificate.certificateKeystoreFile", SSLHostConfig.Type.JSSE);
        setStoreType("Certificate.certificateKeystoreFile", StoreType.KEYSTORE);
        this.certificateKeystoreFile = certificateKeystoreFile;
    }


    public String getCertificateKeystoreFile() {
        return certificateKeystoreFile;
    }


    public void setCertificateKeystorePassword(String certificateKeystorePassword) {
        sslHostConfig.setProperty(
                "Certificate.certificateKeystorePassword", SSLHostConfig.Type.JSSE);
        setStoreType("Certificate.certificateKeystorePassword", StoreType.KEYSTORE);
        this.certificateKeystorePassword = certificateKeystorePassword;
    }


    public String getCertificateKeystorePassword() {
        return certificateKeystorePassword;
    }


    public void setCertificateKeystorePasswordFile(String certificateKeystorePasswordFile) {
        sslHostConfig.setProperty(
                "Certificate.certificateKeystorePasswordFile", SSLHostConfig.Type.JSSE);
        setStoreType("Certificate.certificateKeystorePasswordFile", StoreType.KEYSTORE);
        this.certificateKeystorePasswordFile = certificateKeystorePasswordFile;
    }


    public String getCertificateKeystorePasswordFile() {
        return certificateKeystorePasswordFile;
    }


    public void setCertificateKeystoreProvider(String certificateKeystoreProvider) {
        sslHostConfig.setProperty(
                "Certificate.certificateKeystoreProvider", SSLHostConfig.Type.JSSE);
        setStoreType("Certificate.certificateKeystoreProvider", StoreType.KEYSTORE);
        this.certificateKeystoreProvider = certificateKeystoreProvider;
    }


    public String getCertificateKeystoreProvider() {
        return certificateKeystoreProvider;
    }


    public void setCertificateKeystoreType(String certificateKeystoreType) {
        sslHostConfig.setProperty(
                "Certificate.certificateKeystoreType", SSLHostConfig.Type.JSSE);
        setStoreType("Certificate.certificateKeystoreType", StoreType.KEYSTORE);
        this.certificateKeystoreType = certificateKeystoreType;
    }


    public String getCertificateKeystoreType() {
        return certificateKeystoreType;
    }


    public void setCertificateKeystore(KeyStore certificateKeystore) {
        this.certificateKeystore = certificateKeystore;
        if (certificateKeystore != null) {
            setCertificateKeystoreType(certificateKeystore.getType());
        }
    }


    public KeyStore getCertificateKeystore() throws IOException {
        KeyStore result = certificateKeystore;

        if (result == null && storeType == StoreType.KEYSTORE) {
            result = SSLUtilBase.getStore(getCertificateKeystoreType(),
                    getCertificateKeystoreProvider(), getCertificateKeystoreFile(),
                    getCertificateKeystorePassword(), getCertificateKeystorePasswordFile());
        }

        return result;
    }


    public void setCertificateKeyManager(X509KeyManager certificateKeyManager) {
        this.certificateKeyManager = certificateKeyManager;
    }


    public X509KeyManager getCertificateKeyManager() {
        return certificateKeyManager;
    }


    // OpenSSL

    public void setCertificateChainFile(String certificateChainFile) {
        setStoreType("Certificate.certificateChainFile", StoreType.PEM);
        this.certificateChainFile = certificateChainFile;
    }


    public String getCertificateChainFile() {
        return certificateChainFile;
    }


    public void setCertificateFile(String certificateFile) {
        setStoreType("Certificate.certificateFile", StoreType.PEM);
        this.certificateFile = certificateFile;
    }


    public String getCertificateFile() {
        return certificateFile;
    }


    public void setCertificateKeyFile(String certificateKeyFile) {
        setStoreType("Certificate.certificateKeyFile", StoreType.PEM);
        this.certificateKeyFile = certificateKeyFile;
    }


    public String getCertificateKeyFile() {
        return certificateKeyFile;
    }


    private void setStoreType(String name, StoreType type) {
        if (storeType == null) {
            storeType = type;
        } else if (storeType != type) {
            log.warn(sm.getString("sslHostConfigCertificate.mismatch",
                    name, sslHostConfig.getHostName(), type, this.storeType));
        }
    }

    StoreType getStoreType() {
        return storeType;
    }


    public enum Type {

        UNDEFINED,
        RSA(Authentication.RSA),
        DSA(Authentication.DSS),
        EC(Authentication.ECDH, Authentication.ECDSA);

        private final Set<Authentication> compatibleAuthentications;

        Type(Authentication... authentications) {
            compatibleAuthentications = new HashSet<>();
            if (authentications != null) {
                compatibleAuthentications.addAll(Arrays.asList(authentications));
            }
        }

        public boolean isCompatibleWith(Authentication au) {
            return compatibleAuthentications.contains(au);
        }
    }

    enum StoreType {
        KEYSTORE,
        PEM
    }
}
