/*
 * 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.axis2.testutils;

import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.Date;
import java.util.Random;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;

import org.eclipse.jetty.ee10.servlet.ServletHolder;
import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.transport.http.AxisServlet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

/**
 * Support for running an embedded Jetty server
 */
public class JettyServer extends AbstractAxis2Server {
    /**
     * The alias of the certificate to configure for Jetty's ssl context factory: {@value}
     */
    private static final String CERT_ALIAS = "server";
    
    /**
     * Webapp resource base directory to use: {@value}
     */
    private static final String WEBAPP_DIR = "target" + File.separator + "webapp";
    
    private static final Log log = LogFactory.getLog(JettyServer.class);
    
    private final boolean secure;
    private File keyStoreFile;
    private SSLContext clientSslContext;
    private SslContextFactory.Server serverSslContextFactory;
    private Server server;
    
    /**
     * Constructor.
     * 
     * @param repositoryPath
     *            The path to the Axis2 repository to use. Must not be null or empty.
     * @param secure
     *            Whether to enable HTTPS.
     */
    public JettyServer(String repositoryPath, boolean secure, AxisServiceFactory... axisServiceFactories) {
        super(repositoryPath, axisServiceFactories);
        this.secure = secure;
    }
    
    private String generatePassword(Random random) {
        char[] password = new char[8];
        for (int i=0; i<password.length; i++) {
            password[i] = (char)('0' + random.nextInt(10));
        }
        return new String(password);
    }
    
    private void writeKeyStore(KeyStore keyStore, File file, String password) throws Exception {
        FileOutputStream out = new FileOutputStream(file);
        try {
            keyStore.store(out, password.toCharArray());
        } finally {
            out.close();
        }
    }
    
    private void generateKeys() throws Exception {
        SecureRandom random = new SecureRandom();
        
        // Generate key pair
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(1024, random);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        PublicKey publicKey = keyPair.getPublic();
        
        // Generate certificate
        X500Name dn = new X500Name("cn=localhost,o=Apache");
        BigInteger serial = BigInteger.valueOf(random.nextInt());
        Instant now = Instant.now();
        Date notBefore = Date.from(now);
        Date notAfter = Date.from(now.plusSeconds(3600));
        SubjectPublicKeyInfo subPubKeyInfo =  SubjectPublicKeyInfo.getInstance(publicKey.getEncoded());
        X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(dn, serial, notBefore, notAfter, dn, subPubKeyInfo);
        X509CertificateHolder certHolder = certBuilder.build(new JcaContentSignerBuilder("SHA1WithRSA").build(privateKey));
        X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certHolder);
        
        // Build key store
        keyStoreFile = File.createTempFile("keystore", "jks", null);
        String keyStorePassword = generatePassword(random);
        String keyPassword = generatePassword(random);
        KeyStore keyStore = KeyStore.getInstance("JKS");
        keyStore.load(null, null);
        keyStore.setKeyEntry(CERT_ALIAS, privateKey, keyPassword.toCharArray(), new X509Certificate[] { cert });
        writeKeyStore(keyStore, keyStoreFile, keyStorePassword);
        
        // Build trust store
        KeyStore trustStore = KeyStore.getInstance("JKS");
        trustStore.load(null, null);
        trustStore.setCertificateEntry(CERT_ALIAS, cert);
        
        serverSslContextFactory = new SslContextFactory.Server();
        serverSslContextFactory.setKeyStorePath(keyStoreFile.getAbsolutePath());
        serverSslContextFactory.setKeyStorePassword(keyStorePassword);
        serverSslContextFactory.setKeyManagerPassword(keyPassword);
        serverSslContextFactory.setCertAlias(CERT_ALIAS);
        
        clientSslContext = SSLContext.getInstance("TLS");
        TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmfactory.init(trustStore);
        clientSslContext.init(null, tmfactory.getTrustManagers(), null);
    }
    
    @Override
    public SSLContext getClientSSLContext() throws Exception {
        if (secure) {
            if (clientSslContext == null) {
                generateKeys();
            }
            return clientSslContext;
        } else {
            return null;
        }
    }

    @Override
    protected void startServer(final ConfigurationContext configurationContext) throws Throwable {
        server = new Server();
        
        if (!secure) {
            ServerConnector connector = new ServerConnector(server);
            server.addConnector(connector);
        } else {
            if (serverSslContextFactory == null) {
                generateKeys();
            }
            ServerConnector sslConnector = new ServerConnector(server, serverSslContextFactory);
            server.addConnector(sslConnector);
        }
        
        WebAppContext context = new WebAppContext();
        File webappDir = new File(WEBAPP_DIR);
        if (!webappDir.exists() && !webappDir.mkdirs()) {
            log.error("Failed to create Axis2 webapp directory: " + webappDir.getAbsolutePath());
        }
        
	Path webappPath = Paths.get(webappDir.getAbsolutePath());
        context.setBaseResource(context.getResourceFactory().newResource(webappPath));
        context.setContextPath("/axis2");
        context.setParentLoaderPriority(true);
        context.setThrowUnavailableOnStartupException(true);
        
        @SuppressWarnings("serial")
        ServletHolder servlet = new ServletHolder(new AxisServlet() {
            @Override
            protected ConfigurationContext initConfigContext(ServletConfig config)
                    throws ServletException {
                return configurationContext;
            }
        });
        
        //load on startup to trigger Axis2 initialization and service deployment
        //this is for backward compatibility with the SimpleHttpServer which initializes Axis2 on startup
        servlet.setInitOrder(0);
        
        context.addServlet(servlet, "/services/*");
        
        server.setHandler(context);
        
        try {
            server.start();
        }
        catch (SecurityException e) {
            if (e.getMessage().equals("class \"jakarta.servlet.ServletRequestListener\"'s signer information does not match signer information of other classes in the same package")) {
                log.error(
                 "It is likely your test classpath contains multiple different versions of servlet api.\n" +
                 "If you are running this test in an IDE, please configure it to exclude Rampart's core module servlet api dependency.");
                throw e;
            }
        }
        
        log.info("Server started on port " + getPort());
    }
    
    @Override
    protected void stopServer() {
        if (server != null) {
            log.info("Stop called");
            try {
                server.stop();
            } catch (Exception ex) {
                log.error("Failed to stop Jetty server", ex);
            }
            server = null;
        }
        if (keyStoreFile != null) {
            keyStoreFile.delete();
            keyStoreFile = null;
        }
        clientSslContext = null;
        serverSslContextFactory = null;
    }

    @Override
    public boolean isSecure() {
        return secure;
    }

    /**
     * @return Jetty's http connector port. 
     * @throws IllegalStateException If Jetty is not running or the http connector cannot be found.
     */
    @Override
    public int getPort() throws IllegalStateException {
        if (server == null) {
            throw new IllegalStateException("Jetty server is not initialized");
        }
        if (!server.isStarted()) {
            throw new IllegalStateException("Jetty server is not started");
        }
        
        Connector[] connectors = server.getConnectors();
        if (connectors.length == 0) {
            throw new IllegalStateException("Jetty server is not configured with any connectors");
        }
        
        for (Connector connector : connectors) {
            if (connector instanceof ServerConnector) {
                //must be the http connector
                return ((ServerConnector) connector).getLocalPort();
            }
        }
        
        throw new IllegalStateException("Could not find Jetty http connector");
    }

    @Override
    public String getEndpoint(String serviceName) {
        return String.format("%s://localhost:%s/axis2/services/%s", secure ? "https" : "http", getPort(), serviceName);
    }

    @Override
    public EndpointReference getEndpointReference(String serviceName) {
        return new EndpointReference(getEndpoint(serviceName));
    }
}
